Compare commits
61 Commits
feat/invit
...
0.20.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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 | ||
![]() |
ab01ff249d |
15
.github/workflows/release.yaml
vendored
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]
|
||||
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,4 +11,6 @@ data.db
|
||||
.DS_Store
|
||||
.env.local
|
||||
*.tar.gz
|
||||
.vscode/
|
||||
.vscode/
|
||||
.yalc
|
||||
yalc.lock
|
88
README.md
88
README.md
@@ -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,35 +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. 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`
|
||||
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
|
||||
9. 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:
|
||||
@@ -95,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)
|
||||
|
||||
@@ -115,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
|
||||
|
||||
@@ -131,20 +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`
|
||||
|
||||
Deploy production ready Authorizer instance using one click deployment options available below
|
||||
## Step 2: Setup Instance
|
||||
|
||||
| **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) |
|
||||
- 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
|
||||
|
||||
> 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
|
||||
|
||||
@@ -163,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
|
||||
@@ -175,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();
|
||||
|
30
app/package-lock.json
generated
30
app/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
@@ -24,9 +24,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-js": {
|
||||
"version": "0.4.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
||||
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
||||
"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"
|
||||
},
|
||||
@@ -35,11 +35,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-react": {
|
||||
"version": "0.9.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
||||
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
||||
"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.4.0-beta.3",
|
||||
"@authorizerdev/authorizer-js": "^0.10.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
@@ -829,19 +829,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": {
|
||||
"version": "0.4.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
||||
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
||||
"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.9.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
||||
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
||||
"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.4.0-beta.3",
|
||||
"@authorizerdev/authorizer-js": "^0.10.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.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",
|
||||
|
71
dashboard/package-lock.json
generated
71
dashboard/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"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",
|
||||
@@ -1251,6 +1252,14 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
||||
@@ -1631,6 +1640,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
|
||||
"integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -1914,9 +1934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -1959,6 +1979,22 @@
|
||||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
|
||||
"integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.4.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
@@ -3226,6 +3262,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
||||
@@ -3478,6 +3519,14 @@
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"file-selector": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
|
||||
"integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -3707,9 +3756,9 @@
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -3743,6 +3792,16 @@
|
||||
"scheduler": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
|
||||
"integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==",
|
||||
"requires": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.4.0",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"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
1
dashboard/public/sample.csv
Normal file
@@ -0,0 +1 @@
|
||||
foo@bar.com,test@authorizer.dev
|
|
@@ -10,6 +10,9 @@ const queryClient = createClient({
|
||||
fetchOptions: () => {
|
||||
return {
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'x-authorizer-url': window.location.origin,
|
||||
},
|
||||
};
|
||||
},
|
||||
requestPolicy: 'network-only',
|
||||
|
247
dashboard/src/components/GenerateKeysModal.tsx
Normal file
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;
|
369
dashboard/src/components/InviteMembersModal.tsx
Normal file
369
dashboard/src/components/InviteMembersModal.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
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,
|
||||
} 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%">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;
|
@@ -2,6 +2,7 @@ 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',
|
||||
@@ -60,6 +61,7 @@ export const SwitchInputType = {
|
||||
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',
|
||||
};
|
||||
|
||||
export const DateInputType = {
|
||||
@@ -88,3 +90,41 @@ export const ECDSAEncryptionType = {
|
||||
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;
|
||||
}
|
||||
|
@@ -45,3 +45,37 @@ export const DeleteUser = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -48,10 +48,12 @@ export const EnvVariablesQuery = `
|
||||
DISABLE_MAGIC_LINK_LOGIN,
|
||||
DISABLE_EMAIL_VERIFICATION,
|
||||
DISABLE_BASIC_AUTHENTICATION,
|
||||
DISABLE_SIGN_UP,
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||
DATABASE_NAME,
|
||||
DATABASE_TYPE,
|
||||
DATABASE_URL,
|
||||
ACCESS_TOKEN_EXPIRY_TIME,
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -80,7 +82,16 @@ export const UserDetailsQuery = `
|
||||
signup_methods
|
||||
roles
|
||||
created_at
|
||||
revoked_timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmailVerificationQuery = `
|
||||
query {
|
||||
_env{
|
||||
DISABLE_EMAIL_VERIFICATION
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -34,45 +34,11 @@ import {
|
||||
HMACEncryptionType,
|
||||
RSAEncryptionType,
|
||||
ECDSAEncryptionType,
|
||||
envVarTypes,
|
||||
} from '../constants';
|
||||
import { UpdateEnvVariables } from '../graphql/mutation';
|
||||
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
||||
|
||||
interface envVarTypes {
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_CLIENT_SECRET: string;
|
||||
GITHUB_CLIENT_ID: string;
|
||||
GITHUB_CLIENT_SECRET: string;
|
||||
FACEBOOK_CLIENT_ID: string;
|
||||
FACEBOOK_CLIENT_SECRET: string;
|
||||
ROLES: [string] | [];
|
||||
DEFAULT_ROLES: [string] | [];
|
||||
PROTECTED_ROLES: [string] | [];
|
||||
JWT_TYPE: string;
|
||||
JWT_SECRET: string;
|
||||
JWT_ROLE_CLAIM: string;
|
||||
JWT_PRIVATE_KEY: string;
|
||||
JWT_PUBLIC_KEY: string;
|
||||
REDIS_URL: string;
|
||||
SMTP_HOST: string;
|
||||
SMTP_PORT: string;
|
||||
SMTP_USERNAME: string;
|
||||
SMTP_PASSWORD: string;
|
||||
SENDER_EMAIL: string;
|
||||
ALLOWED_ORIGINS: [string] | [];
|
||||
ORGANIZATION_NAME: string;
|
||||
ORGANIZATION_LOGO: string;
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT: string;
|
||||
ADMIN_SECRET: string;
|
||||
DISABLE_LOGIN_PAGE: boolean;
|
||||
DISABLE_MAGIC_LINK_LOGIN: boolean;
|
||||
DISABLE_EMAIL_VERIFICATION: boolean;
|
||||
DISABLE_BASIC_AUTHENTICATION: boolean;
|
||||
OLD_ADMIN_SECRET: string;
|
||||
DATABASE_NAME: string;
|
||||
DATABASE_TYPE: string;
|
||||
DATABASE_URL: string;
|
||||
}
|
||||
import GenerateKeysModal from '../components/GenerateKeysModal';
|
||||
|
||||
export default function Environment() {
|
||||
const client = useClient();
|
||||
@@ -114,10 +80,12 @@ export default function Environment() {
|
||||
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<
|
||||
@@ -132,32 +100,24 @@ export default function Environment() {
|
||||
OLD_ADMIN_SECRET: false,
|
||||
});
|
||||
|
||||
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(() => {
|
||||
let isMounted = true;
|
||||
async function getData() {
|
||||
const {
|
||||
data: { _env: envData },
|
||||
} = await client.query(EnvVariablesQuery).toPromise();
|
||||
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
setEnvVariables({
|
||||
...envData,
|
||||
OLD_ADMIN_SECRET: envData.ADMIN_SECRET,
|
||||
ADMIN_SECRET: '',
|
||||
});
|
||||
setAdminSecret({
|
||||
value: '',
|
||||
disableInputField: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getData();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const validateAdminSecretHandler = (event: any) => {
|
||||
@@ -228,6 +188,8 @@ export default function Environment() {
|
||||
disableInputField: true,
|
||||
});
|
||||
|
||||
getData();
|
||||
|
||||
toast({
|
||||
title: `Successfully updated ${
|
||||
Object.keys(updatedEnvVariables).length
|
||||
@@ -254,7 +216,7 @@ export default function Environment() {
|
||||
setVariables={() => {}}
|
||||
inputType={TextInputType.CLIENT_ID}
|
||||
placeholder="Client ID"
|
||||
isDisabled={true}
|
||||
readOnly={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
@@ -270,7 +232,7 @@ export default function Environment() {
|
||||
setFieldVisibility={setFieldVisibility}
|
||||
inputType={HiddenInputType.CLIENT_SECRET}
|
||||
placeholder="Client Secret"
|
||||
isDisabled={true}
|
||||
readOnly={true}
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
@@ -408,9 +370,22 @@ export default function Environment() {
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Divider marginTop="2%" marginBottom="2%" />
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
JWT (JSON Web Tokens) Configurations
|
||||
</Text>
|
||||
<Flex
|
||||
width="100%"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
paddingTop="2%"
|
||||
>
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
JWT (JSON Web Tokens) Configurations
|
||||
</Text>
|
||||
<Flex>
|
||||
<GenerateKeysModal
|
||||
jwtType={envVariables.JWT_TYPE}
|
||||
getData={getData}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
@@ -626,11 +601,28 @@ export default function Environment() {
|
||||
</Stack>
|
||||
<Divider marginTop="2%" marginBottom="2%" />
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
Custom Access Token Scripts
|
||||
Access Token
|
||||
</Text>
|
||||
<Stack spacing={6} padding="2% 0%">
|
||||
<Flex>
|
||||
<Center w="100%">
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Access Token Expiry Time:</Text>
|
||||
</Flex>
|
||||
<Flex w="70%">
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
inputType={TextInputType.ACCESS_TOKEN_EXPIRY_TIME}
|
||||
placeholder="0h15m0s"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="30%" justifyContent="start" direction="column">
|
||||
<Text fontSize="sm">Custom Scripts:</Text>
|
||||
<Text fontSize="sm">Used to add custom fields in ID token</Text>
|
||||
</Flex>
|
||||
<Flex w="70%">
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
@@ -638,7 +630,7 @@ export default function Environment() {
|
||||
placeholder="Add script here"
|
||||
minH="25vh"
|
||||
/>
|
||||
</Center>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Divider marginTop="2%" marginBottom="2%" />
|
||||
@@ -694,6 +686,18 @@ export default function Environment() {
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Sign Up:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start" w="70%">
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
inputType={SwitchInputType.DISABLE_SIGN_UP}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Divider marginTop="2%" marginBottom="2%" />
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
|
@@ -38,10 +38,11 @@ import {
|
||||
FaExclamationCircle,
|
||||
FaAngleDown,
|
||||
} from 'react-icons/fa';
|
||||
import { UserDetailsQuery } from '../graphql/queries';
|
||||
import { UpdateUser } from '../graphql/mutation';
|
||||
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;
|
||||
@@ -66,6 +67,12 @@ interface userDataTypes {
|
||||
signup_methods: string;
|
||||
roles: [string];
|
||||
created_at: number;
|
||||
revoked_timestamp: number;
|
||||
}
|
||||
|
||||
const enum updateAccessActions {
|
||||
REVOKE = 'REVOKE',
|
||||
ENABLE = 'ENABLE',
|
||||
}
|
||||
|
||||
const getMaxPages = (pagination: paginationPropTypes) => {
|
||||
@@ -101,6 +108,8 @@ export default function Users() {
|
||||
});
|
||||
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
|
||||
@@ -132,8 +141,18 @@ export default function Users() {
|
||||
}
|
||||
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();
|
||||
@@ -171,12 +190,77 @@ export default function Users() {
|
||||
}
|
||||
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 ? (
|
||||
@@ -188,6 +272,7 @@ export default function Users() {
|
||||
<Th>Signup Methods</Th>
|
||||
<Th>Roles</Th>
|
||||
<Th>Verified</Th>
|
||||
<Th>Access</Th>
|
||||
<Th>Actions</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -196,7 +281,7 @@ export default function Users() {
|
||||
const { email_verified, created_at, ...rest }: any = user;
|
||||
return (
|
||||
<Tr key={user.id} style={{ fontSize: 14 }}>
|
||||
<Td>{user.email}</Td>
|
||||
<Td maxW="300">{user.email}</Td>
|
||||
<Td>
|
||||
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
|
||||
</Td>
|
||||
@@ -211,6 +296,15 @@ export default function Users() {
|
||||
{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">
|
||||
@@ -240,6 +334,29 @@ export default function Users() {
|
||||
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>
|
||||
|
@@ -64,3 +64,25 @@ export const getObjectDiff = (obj1: any, obj2: any) => {
|
||||
|
||||
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
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;
|
@@ -17,4 +17,6 @@ const (
|
||||
DbTypeYugabyte = "yugabyte"
|
||||
// DbTypeMariaDB is the mariadb database type
|
||||
DbTypeMariaDB = "mariadb"
|
||||
// DbTypeCassandra is the cassandra database type
|
||||
DbTypeCassandraDB = "cassandradb"
|
||||
)
|
||||
|
@@ -16,11 +16,12 @@ const (
|
||||
// EnvKeyEnvPath key for cli arg variable ENV_PATH
|
||||
EnvKeyEnvPath = "ENV_PATH"
|
||||
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
|
||||
// TODO: remove support AUTHORIZER_URL env
|
||||
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
|
||||
@@ -29,6 +30,14 @@ 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"
|
||||
// EnvKeySmtpHost key for env variable SMTP_HOST
|
||||
EnvKeySmtpHost = "SMTP_HOST"
|
||||
// EnvKeySmtpPort key for env variable SMTP_PORT
|
||||
@@ -67,6 +76,8 @@ const (
|
||||
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"
|
||||
// EnvKeyRoles key for env variable ROLES
|
||||
EnvKeyRoles = "ROLES"
|
||||
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"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"
|
||||
@@ -15,9 +16,10 @@ var Provider providers.Provider
|
||||
func InitDB() error {
|
||||
var err error
|
||||
|
||||
isSQL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeArangodb && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeMongodb
|
||||
isSQL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeArangodb && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeMongodb && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) != constants.DbTypeCassandraDB
|
||||
isArangoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeArangodb
|
||||
isMongoDB := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeMongodb
|
||||
isCassandra := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseType) == constants.DbTypeCassandraDB
|
||||
|
||||
if isSQL {
|
||||
Provider, err = sql.NewProvider()
|
||||
@@ -40,5 +42,12 @@ func InitDB() error {
|
||||
}
|
||||
}
|
||||
|
||||
if isCassandra {
|
||||
Provider, err = cassandradb.NewProvider()
|
||||
if err != nil {
|
||||
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 string `gorm:"type:text" json:"env" bson:"env"`
|
||||
Hash string `gorm:"type:text" json:"hash" bson:"hash"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||
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 `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" bson:"-"`
|
||||
UserAgent string `json:"user_agent" bson:"user_agent"`
|
||||
IP string `json:"ip" bson:"ip"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
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"`
|
||||
}
|
||||
|
@@ -6,32 +6,39 @@ 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
|
||||
|
||||
// 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 `json:"updated_at" bson:"updated_at"`
|
||||
CreatedAt int64 `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,
|
||||
@@ -41,14 +48,15 @@ func (user *User) AsAPIUser() *model.User {
|
||||
FamilyName: user.FamilyName,
|
||||
MiddleName: user.MiddleName,
|
||||
Nickname: user.Nickname,
|
||||
PreferredUsername: &user.Email,
|
||||
PreferredUsername: &email,
|
||||
Gender: user.Gender,
|
||||
Birthdate: user.Birthdate,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
PhoneNumberVerified: &isPhoneVerified,
|
||||
Picture: user.Picture,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
RevokedTimestamp: revokedTimestamp,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
}
|
||||
|
@@ -2,30 +2,40 @@ 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 `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||
Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
|
||||
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
|
||||
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: &v.Token,
|
||||
Identifier: &v.Identifier,
|
||||
Expires: &v.ExpiresAt,
|
||||
CreatedAt: &v.CreatedAt,
|
||||
UpdatedAt: &v.UpdatedAt,
|
||||
Email: &v.Email,
|
||||
Nonce: &v.Nonce,
|
||||
RedirectURI: &v.RedirectURI,
|
||||
Token: &token,
|
||||
Identifier: &identifier,
|
||||
Expires: &expires,
|
||||
Email: &email,
|
||||
Nonce: &nonce,
|
||||
RedirectURI: &redirectURI,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
}
|
||||
|
52
server/db/providers/cassandradb/env.go
Normal file
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
|
||||
}
|
116
server/db/providers/cassandradb/provider.go
Normal file
116
server/db/providers/cassandradb/provider.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
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 := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseURL)
|
||||
KeySpace = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseName)
|
||||
clusterURL := []string{}
|
||||
if strings.Contains(dbURL, ",") {
|
||||
clusterURL = strings.Split(dbURL, ",")
|
||||
} else {
|
||||
clusterURL = append(clusterURL, dbURL)
|
||||
}
|
||||
cassandraClient := cansandraDriver.NewCluster(clusterURL...)
|
||||
if envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseUsername) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabasePassword) != "" {
|
||||
cassandraClient.Authenticator = &cansandraDriver.PasswordAuthenticator{
|
||||
Username: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabaseUsername),
|
||||
Password: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyDatabasePassword),
|
||||
}
|
||||
}
|
||||
|
||||
cassandraClient.RetryPolicy = &cansandraDriver.SimpleRetryPolicy{
|
||||
NumRetries: 3,
|
||||
}
|
||||
cassandraClient.Consistency = cansandraDriver.Quorum
|
||||
|
||||
session, err := cassandraClient.CreateSession()
|
||||
if err != nil {
|
||||
log.Println("Error while creating connection to cassandra db", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyspaceQuery := fmt.Sprintf("CREATE KEYSPACE IF NOT EXISTS %s WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor':1}",
|
||||
KeySpace)
|
||||
err = session.Query(keyspaceQuery).Exec()
|
||||
if err != nil {
|
||||
log.Println("Unable to create keyspace:", err)
|
||||
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 {
|
||||
log.Println("Unable to create env collection:", err)
|
||||
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 {
|
||||
log.Println("Unable to create session collection:", err)
|
||||
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 {
|
||||
log.Println("Unable to create user collection:", err)
|
||||
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 {
|
||||
log.Println("Unable to create user index:", err)
|
||||
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 {
|
||||
log.Println("Unable to create verification request collection:", err)
|
||||
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 {
|
||||
log.Println("Unable to create verification_requests index:", err)
|
||||
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 {
|
||||
log.Println("Unable to create verification_requests index:", err)
|
||||
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 {
|
||||
log.Println("Unable to create verification_requests index:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &provider{
|
||||
db: session,
|
||||
}, err
|
||||
}
|
36
server/db/providers/cassandradb/session.go
Normal file
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
|
||||
}
|
189
server/db/providers/cassandradb/user.go
Normal file
189
server/db/providers/cassandradb/user.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"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 == "" {
|
||||
user.Roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
102
server/db/providers/cassandradb/verification_requests.go
Normal file
102
server/db/providers/cassandradb/verification_requests.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package cassandradb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"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 {
|
||||
log.Println("Error while quering verification request", err)
|
||||
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 {
|
||||
log.Println("Error while parsing verification request", err)
|
||||
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
|
||||
}
|
32
server/db/providers/provider_template/env.go
Normal file
32
server/db/providers/provider_template/env.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AddEnv to save environment information in database
|
||||
func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
||||
if env.ID == "" {
|
||||
env.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
env.CreatedAt = time.Now().Unix()
|
||||
env.UpdatedAt = time.Now().Unix()
|
||||
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()
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// GetEnv to get environment information from database
|
||||
func (p *provider) GetEnv() (models.Env, error) {
|
||||
var env models.Env
|
||||
|
||||
return env, nil
|
||||
}
|
20
server/db/providers/provider_template/provider.go
Normal file
20
server/db/providers/provider_template/provider.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TODO change following provider to new db provider
|
||||
type provider struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewProvider returns a new SQL provider
|
||||
// TODO change following provider to new db provider
|
||||
func NewProvider() (*provider, error) {
|
||||
var sqlDB *gorm.DB
|
||||
|
||||
return &provider{
|
||||
db: sqlDB,
|
||||
}, nil
|
||||
}
|
24
server/db/providers/provider_template/session.go
Normal file
24
server/db/providers/provider_template/session.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"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()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteSession to delete session information from database
|
||||
func (p *provider) DeleteSession(userId string) error {
|
||||
return nil
|
||||
}
|
58
server/db/providers/provider_template/user.go
Normal file
58
server/db/providers/provider_template/user.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AddUser to save user information in database
|
||||
func (p *provider) AddUser(user models.User) (models.User, error) {
|
||||
if user.ID == "" {
|
||||
user.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
if user.Roles == "" {
|
||||
user.Roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||
}
|
||||
|
||||
user.CreatedAt = time.Now().Unix()
|
||||
user.UpdatedAt = time.Now().Unix()
|
||||
|
||||
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()
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeleteUser to delete user information from database
|
||||
func (p *provider) DeleteUser(user models.User) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListUsers to get list of users from database
|
||||
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail to get user information from database using email address
|
||||
func (p *provider) GetUserByEmail(email string) (models.User, error) {
|
||||
var user models.User
|
||||
|
||||
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
|
||||
|
||||
return user, nil
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
package provider_template
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AddVerification to save verification request in database
|
||||
func (p *provider) AddVerificationRequest(verificationRequest models.VerificationRequest) (models.VerificationRequest, error) {
|
||||
if verificationRequest.ID == "" {
|
||||
verificationRequest.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
verificationRequest.CreatedAt = time.Now().Unix()
|
||||
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
return verificationRequest, nil
|
||||
}
|
||||
|
||||
// ListVerificationRequests to get list of verification requests from database
|
||||
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteVerificationRequest to delete verification request from database
|
||||
func (p *provider) DeleteVerificationRequest(verificationRequest models.VerificationRequest) error {
|
||||
return nil
|
||||
}
|
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// InviteEmail to send invite email
|
||||
func InviteEmail(toEmail, token, url string) error {
|
||||
func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
|
||||
// The receiver needs to be in slice as the receive supports multiple receiver
|
||||
Receiver := []string{toEmail}
|
||||
|
||||
@@ -101,13 +101,13 @@ func InviteEmail(toEmail, token, url string) error {
|
||||
data := make(map[string]interface{}, 3)
|
||||
data["org_logo"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
|
||||
data["org_name"] = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
|
||||
data["verification_url"] = url + "?token=" + token
|
||||
data["verification_url"] = verificationURL + "?token=" + token + "&redirect_uri=" + redirectURI
|
||||
message = addEmailTemplate(message, data, "invite_email.tmpl")
|
||||
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
|
||||
|
||||
err := SendMail(Receiver, Subject, message)
|
||||
if err != nil {
|
||||
log.Println("=> error sending email:", err)
|
||||
log.Println("error sending email:", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@@ -107,7 +107,7 @@ func SendVerificationMail(toEmail, token, hostname string) error {
|
||||
|
||||
err := SendMail(Receiver, Subject, message)
|
||||
if err != nil {
|
||||
log.Println("=> error sending email:", err)
|
||||
log.Println("error sending email:", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
22
server/env/env.go
vendored
22
server/env/env.go
vendored
@@ -38,6 +38,10 @@ func InitRequiredEnv() error {
|
||||
dbURL := os.Getenv(constants.EnvKeyDatabaseURL)
|
||||
dbType := os.Getenv(constants.EnvKeyDatabaseType)
|
||||
dbName := os.Getenv(constants.EnvKeyDatabaseName)
|
||||
dbPort := os.Getenv(constants.EnvKeyDatabasePort)
|
||||
dbHost := os.Getenv(constants.EnvKeyDatabaseHost)
|
||||
dbUsername := os.Getenv(constants.EnvKeyDatabaseUsername)
|
||||
dbPassword := os.Getenv(constants.EnvKeyDatabasePassword)
|
||||
|
||||
if strings.TrimSpace(dbType) == "" {
|
||||
if envstore.ARG_DB_TYPE != nil && *envstore.ARG_DB_TYPE != "" {
|
||||
@@ -54,7 +58,7 @@ func InitRequiredEnv() error {
|
||||
dbURL = strings.TrimSpace(*envstore.ARG_DB_URL)
|
||||
}
|
||||
|
||||
if dbURL == "" {
|
||||
if dbURL == "" && dbPort == "" && dbHost == "" && dbUsername == "" && dbPassword == "" {
|
||||
return errors.New("invalid database url. DATABASE_URL is required")
|
||||
}
|
||||
}
|
||||
@@ -69,6 +73,10 @@ func InitRequiredEnv() error {
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseType, dbType)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseName, dbName)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseHost, dbHost)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabasePort, dbPort)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseUsername, dbUsername)
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabasePassword, dbPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -113,6 +121,10 @@ func InitAllEnv() error {
|
||||
envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL)
|
||||
}
|
||||
|
||||
if envData.StringEnv[constants.EnvKeyAuthorizerURL] == "" {
|
||||
envData.StringEnv[constants.EnvKeyAuthorizerURL] = os.Getenv(constants.EnvKeyAuthorizerURL)
|
||||
}
|
||||
|
||||
if envData.StringEnv[constants.EnvKeyPort] == "" {
|
||||
envData.StringEnv[constants.EnvKeyPort] = os.Getenv(constants.EnvKeyPort)
|
||||
if envData.StringEnv[constants.EnvKeyPort] == "" {
|
||||
@@ -120,6 +132,13 @@ func InitAllEnv() error {
|
||||
}
|
||||
}
|
||||
|
||||
if envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] == "" {
|
||||
envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] = os.Getenv(constants.EnvKeyAccessTokenExpiryTime)
|
||||
if envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] == "" {
|
||||
envData.StringEnv[constants.EnvKeyAccessTokenExpiryTime] = "30m"
|
||||
}
|
||||
}
|
||||
|
||||
if envData.StringEnv[constants.EnvKeyAdminSecret] == "" {
|
||||
envData.StringEnv[constants.EnvKeyAdminSecret] = os.Getenv(constants.EnvKeyAdminSecret)
|
||||
}
|
||||
@@ -281,6 +300,7 @@ func InitAllEnv() error {
|
||||
envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableLoginPage] = os.Getenv(constants.EnvKeyDisableLoginPage) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableSignUp] = os.Getenv(constants.EnvKeyDisableSignUp) == "true"
|
||||
|
||||
// no need to add nil check as its already done above
|
||||
if envData.StringEnv[constants.EnvKeySmtpHost] == "" || envData.StringEnv[constants.EnvKeySmtpUsername] == "" || envData.StringEnv[constants.EnvKeySmtpPassword] == "" || envData.StringEnv[constants.EnvKeySenderEmail] == "" && envData.StringEnv[constants.EnvKeySmtpPort] == "" {
|
||||
|
1
server/env/persist_env.go
vendored
1
server/env/persist_env.go
vendored
@@ -165,6 +165,7 @@ func PersistEnv() error {
|
||||
hasChanged = true
|
||||
}
|
||||
}
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvStore(storeData)
|
||||
jwk, err := crypto.GenerateJWKBasedOnEnv()
|
||||
if err != nil {
|
||||
|
@@ -41,6 +41,7 @@ var defaultStore = &EnvStore{
|
||||
constants.EnvKeyDisableMagicLinkLogin: false,
|
||||
constants.EnvKeyDisableEmailVerification: false,
|
||||
constants.EnvKeyDisableLoginPage: false,
|
||||
constants.EnvKeyDisableSignUp: false,
|
||||
},
|
||||
SliceEnv: map[string][]string{},
|
||||
},
|
||||
|
@@ -9,6 +9,7 @@ require (
|
||||
github.com/gin-gonic/gin v1.7.2
|
||||
github.com/go-playground/validator/v10 v10.8.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.0
|
||||
github.com/gocql/gocql v1.0.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0
|
||||
|
@@ -48,6 +48,10 @@ github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e h1:Xg+hGrY2
|
||||
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:mq7Shfa/CaixoDxiyAAc5jZ6CVBAyPaNQCGS7mkj4Ho=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
|
||||
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -106,6 +110,8 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC
|
||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c=
|
||||
github.com/gocql/gocql v1.0.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
@@ -140,8 +146,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -175,6 +182,8 @@ github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB
|
||||
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -677,6 +686,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||
gopkg.in/readline.v1 v1.0.0-20160726135117-62c6fe619375/go.mod h1:lNEQeAhU009zbRxng+XOj5ITVgY24WcbNnQopyfKoYQ=
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,15 @@ type DeleteUserInput struct {
|
||||
}
|
||||
|
||||
type Env struct {
|
||||
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
|
||||
AdminSecret *string `json:"ADMIN_SECRET"`
|
||||
DatabaseName string `json:"DATABASE_NAME"`
|
||||
DatabaseURL string `json:"DATABASE_URL"`
|
||||
DatabaseType string `json:"DATABASE_TYPE"`
|
||||
DatabaseUsername string `json:"DATABASE_USERNAME"`
|
||||
DatabasePassword string `json:"DATABASE_PASSWORD"`
|
||||
DatabaseHost string `json:"DATABASE_HOST"`
|
||||
DatabasePort string `json:"DATABASE_PORT"`
|
||||
ClientID string `json:"CLIENT_ID"`
|
||||
ClientSecret string `json:"CLIENT_SECRET"`
|
||||
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
|
||||
@@ -49,6 +54,7 @@ type Env struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -74,6 +80,16 @@ type ForgotPasswordInput struct {
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type GenerateJWTKeysInput struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GenerateJWTKeysResponse struct {
|
||||
Secret *string `json:"secret"`
|
||||
PublicKey *string `json:"public_key"`
|
||||
PrivateKey *string `json:"private_key"`
|
||||
}
|
||||
|
||||
type InviteMemberInput struct {
|
||||
Emails []string `json:"emails"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
@@ -103,6 +119,7 @@ type Meta struct {
|
||||
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
|
||||
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
|
||||
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
||||
IsSignUpEnabled bool `json:"is_sign_up_enabled"`
|
||||
}
|
||||
|
||||
type OAuthRevokeInput struct {
|
||||
@@ -159,9 +176,15 @@ type SignUpInput struct {
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
Roles []string `json:"roles"`
|
||||
Scope []string `json:"scope"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type UpdateAccessInput struct {
|
||||
UserID string `json:"user_id"`
|
||||
}
|
||||
|
||||
type UpdateEnvInput struct {
|
||||
AccessTokenExpiryTime *string `json:"ACCESS_TOKEN_EXPIRY_TIME"`
|
||||
AdminSecret *string `json:"ADMIN_SECRET"`
|
||||
CustomAccessTokenScript *string `json:"CUSTOM_ACCESS_TOKEN_SCRIPT"`
|
||||
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
|
||||
@@ -183,6 +206,7 @@ type UpdateEnvInput struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -245,6 +269,7 @@ type User struct {
|
||||
Roles []string `json:"roles"`
|
||||
CreatedAt *int64 `json:"created_at"`
|
||||
UpdatedAt *int64 `json:"updated_at"`
|
||||
RevokedTimestamp *int64 `json:"revoked_timestamp"`
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
@@ -252,6 +277,16 @@ type Users struct {
|
||||
Users []*User `json:"users"`
|
||||
}
|
||||
|
||||
type ValidateJWTTokenInput struct {
|
||||
TokenType string `json:"token_type"`
|
||||
Token string `json:"token"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse struct {
|
||||
IsValid bool `json:"is_valid"`
|
||||
}
|
||||
|
||||
type VerificationRequest struct {
|
||||
ID string `json:"id"`
|
||||
Identifier *string `json:"identifier"`
|
||||
|
@@ -21,6 +21,7 @@ type Meta {
|
||||
is_email_verification_enabled: Boolean!
|
||||
is_basic_authentication_enabled: Boolean!
|
||||
is_magic_link_login_enabled: Boolean!
|
||||
is_sign_up_enabled: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
@@ -42,6 +43,7 @@ type User {
|
||||
roles: [String!]!
|
||||
created_at: Int64
|
||||
updated_at: Int64
|
||||
revoked_timestamp: Int64
|
||||
}
|
||||
|
||||
type Users {
|
||||
@@ -85,10 +87,15 @@ type Response {
|
||||
}
|
||||
|
||||
type Env {
|
||||
ACCESS_TOKEN_EXPIRY_TIME: String
|
||||
ADMIN_SECRET: String
|
||||
DATABASE_NAME: String!
|
||||
DATABASE_URL: String!
|
||||
DATABASE_TYPE: String!
|
||||
DATABASE_USERNAME: String!
|
||||
DATABASE_PASSWORD: String!
|
||||
DATABASE_HOST: String!
|
||||
DATABASE_PORT: String!
|
||||
CLIENT_ID: String!
|
||||
CLIENT_SECRET: String!
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT: String
|
||||
@@ -110,6 +117,7 @@ type Env {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -124,7 +132,18 @@ type Env {
|
||||
ORGANIZATION_LOGO: String
|
||||
}
|
||||
|
||||
type ValidateJWTTokenResponse {
|
||||
is_valid: Boolean!
|
||||
}
|
||||
|
||||
type GenerateJWTKeysResponse {
|
||||
secret: String
|
||||
public_key: String
|
||||
private_key: String
|
||||
}
|
||||
|
||||
input UpdateEnvInput {
|
||||
ACCESS_TOKEN_EXPIRY_TIME: String
|
||||
ADMIN_SECRET: String
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT: String
|
||||
OLD_ADMIN_SECRET: String
|
||||
@@ -146,6 +165,7 @@ input UpdateEnvInput {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -182,6 +202,7 @@ input SignUpInput {
|
||||
confirm_password: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
@@ -277,6 +298,20 @@ input InviteMemberInput {
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input UpdateAccessInput {
|
||||
user_id: String!
|
||||
}
|
||||
|
||||
input ValidateJWTTokenInput {
|
||||
token_type: String!
|
||||
token: String!
|
||||
roles: [String!]
|
||||
}
|
||||
|
||||
input GenerateJWTKeysInput {
|
||||
type: String!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signup(params: SignUpInput!): AuthResponse!
|
||||
login(params: LoginInput!): AuthResponse!
|
||||
@@ -296,12 +331,16 @@ type Mutation {
|
||||
_admin_logout: Response!
|
||||
_update_env(params: UpdateEnvInput!): Response!
|
||||
_invite_members(params: InviteMemberInput!): Response!
|
||||
_revoke_access(param: UpdateAccessInput!): Response!
|
||||
_enable_access(param: UpdateAccessInput!): Response!
|
||||
_generate_jwt_keys(params: GenerateJWTKeysInput!): GenerateJWTKeysResponse!
|
||||
}
|
||||
|
||||
type Query {
|
||||
meta: Meta!
|
||||
session(params: SessionQueryInput): AuthResponse!
|
||||
profile: User!
|
||||
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
|
||||
# admin only apis
|
||||
_users(params: PaginatedInput): Users!
|
||||
_verification_requests(params: PaginatedInput): VerificationRequests!
|
||||
|
@@ -79,6 +79,18 @@ func (r *mutationResolver) InviteMembers(ctx context.Context, params model.Invit
|
||||
return resolvers.InviteMembersResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) RevokeAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) {
|
||||
return resolvers.RevokeAccessResolver(ctx, param)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) EnableAccess(ctx context.Context, param model.UpdateAccessInput) (*model.Response, error) {
|
||||
return resolvers.EnableAccessResolver(ctx, param)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GenerateJwtKeys(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) {
|
||||
return resolvers.GenerateJWTKeysResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
|
||||
return resolvers.MetaResolver(ctx)
|
||||
}
|
||||
@@ -91,6 +103,10 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
|
||||
return resolvers.ProfileResolver(ctx)
|
||||
}
|
||||
|
||||
func (r *queryResolver) ValidateJwtToken(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) {
|
||||
return resolvers.ValidateJwtTokenResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
|
||||
return resolvers.UsersResolver(ctx, params)
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
@@ -276,7 +277,11 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
expiresIn := int64(1800)
|
||||
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
// used of query mode
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
|
||||
|
@@ -71,6 +71,10 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
existingUser, err := db.Provider.GetUserByEmail(user.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
c.JSON(400, gin.H{"error": "signup is disabled for this instance"})
|
||||
return
|
||||
}
|
||||
// user not registered, register user and generate session token
|
||||
user.SignupMethods = provider
|
||||
// make sure inputRoles don't include protected roles
|
||||
@@ -91,9 +95,12 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
user.EmailVerifiedAt = &now
|
||||
user, _ = db.Provider.AddUser(user)
|
||||
} else {
|
||||
if user.RevokedTimestamp != nil {
|
||||
c.JSON(400, gin.H{"error": "user access has been revoked"})
|
||||
}
|
||||
|
||||
// user exists in db, check if method was google
|
||||
// if not append google to existing signup method and save it
|
||||
|
||||
signupMethod := existingUser.SignupMethods
|
||||
if !strings.Contains(signupMethod, provider) {
|
||||
signupMethod = signupMethod + "," + provider
|
||||
@@ -150,7 +157,12 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
}
|
||||
expiresIn := int64(1800)
|
||||
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token
|
||||
|
||||
cookie.SetSession(c, authToken.FingerPrintHash)
|
||||
@@ -247,7 +259,7 @@ func processGithubUserInfo(code string) (models.User, error) {
|
||||
GivenName: &firstName,
|
||||
FamilyName: &lastName,
|
||||
Picture: &picture,
|
||||
Email: userRawData["sub"],
|
||||
Email: userRawData["email"],
|
||||
}
|
||||
|
||||
return user, nil
|
||||
|
@@ -16,7 +16,11 @@ import (
|
||||
func OAuthLoginHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
hostname := utils.GetHost(c)
|
||||
// deprecating redirectURL instead use redirect_uri
|
||||
redirectURI := strings.TrimSpace(c.Query("redirectURL"))
|
||||
if redirectURI == "" {
|
||||
redirectURI = strings.TrimSpace(c.Query("redirect_uri"))
|
||||
}
|
||||
roles := strings.TrimSpace(c.Query("roles"))
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
@@ -110,8 +111,6 @@ func TokenHandler() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
// validate session
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
||||
if err != nil {
|
||||
@@ -121,6 +120,8 @@ func TokenHandler() gin.HandlerFunc {
|
||||
})
|
||||
return
|
||||
}
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
userID = claims.Subject
|
||||
roles = claims.Roles
|
||||
scope = claims.Scope
|
||||
@@ -174,7 +175,11 @@ func TokenHandler() gin.HandlerFunc {
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
res := map[string]interface{}{
|
||||
"access_token": authToken.AccessToken.Token,
|
||||
"id_token": authToken.IDToken.Token,
|
||||
|
@@ -82,7 +82,12 @@ func VerifyEmailHandler() gin.HandlerFunc {
|
||||
c.JSON(500, errorRes)
|
||||
return
|
||||
}
|
||||
expiresIn := int64(1800)
|
||||
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
|
||||
|
||||
cookie.SetSession(c, authToken.FingerPrintHash)
|
||||
|
@@ -15,7 +15,7 @@ func CORSMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, X-authorizer-url")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
|
44
server/resolvers/enable_access.go
Normal file
44
server/resolvers/enable_access.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// EnableAccessResolver is a resolver for enabling user access
|
||||
func EnableAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
var res *model.Response
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return res, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
user, err := db.Provider.GetUserByID(params.UserID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
user.RevokedTimestamp = nil
|
||||
|
||||
user, err = db.Provider.UpdateUser(user)
|
||||
if err != nil {
|
||||
log.Println("error updating user:", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
res = &model.Response{
|
||||
Message: `user access enabled successfully`,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@@ -27,12 +27,17 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
|
||||
// get clone of store
|
||||
store := envstore.EnvStoreObj.GetEnvStoreClone()
|
||||
accessTokenExpiryTime := store.StringEnv[constants.EnvKeyAccessTokenExpiryTime]
|
||||
adminSecret := store.StringEnv[constants.EnvKeyAdminSecret]
|
||||
clientID := store.StringEnv[constants.EnvKeyClientID]
|
||||
clientSecret := store.StringEnv[constants.EnvKeyClientSecret]
|
||||
databaseURL := store.StringEnv[constants.EnvKeyDatabaseURL]
|
||||
databaseName := store.StringEnv[constants.EnvKeyDatabaseName]
|
||||
databaseType := store.StringEnv[constants.EnvKeyDatabaseType]
|
||||
databaseUsername := store.StringEnv[constants.EnvKeyDatabaseUsername]
|
||||
databasePassword := store.StringEnv[constants.EnvKeyDatabasePassword]
|
||||
databaseHost := store.StringEnv[constants.EnvKeyDatabaseHost]
|
||||
databasePort := store.StringEnv[constants.EnvKeyDatabasePort]
|
||||
customAccessTokenScript := store.StringEnv[constants.EnvKeyCustomAccessTokenScript]
|
||||
smtpHost := store.StringEnv[constants.EnvKeySmtpHost]
|
||||
smtpPort := store.StringEnv[constants.EnvKeySmtpPort]
|
||||
@@ -53,6 +58,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]
|
||||
disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]
|
||||
disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage]
|
||||
disableSignUp := store.BoolEnv[constants.EnvKeyDisableSignUp]
|
||||
roles := store.SliceEnv[constants.EnvKeyRoles]
|
||||
defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles]
|
||||
protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles]
|
||||
@@ -65,11 +71,20 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
organizationName := store.StringEnv[constants.EnvKeyOrganizationName]
|
||||
organizationLogo := store.StringEnv[constants.EnvKeyOrganizationLogo]
|
||||
|
||||
if accessTokenExpiryTime == "" {
|
||||
accessTokenExpiryTime = "30m"
|
||||
}
|
||||
|
||||
res = &model.Env{
|
||||
AccessTokenExpiryTime: &accessTokenExpiryTime,
|
||||
AdminSecret: &adminSecret,
|
||||
DatabaseName: databaseName,
|
||||
DatabaseURL: databaseURL,
|
||||
DatabaseType: databaseType,
|
||||
DatabaseUsername: databaseUsername,
|
||||
DatabasePassword: databasePassword,
|
||||
DatabaseHost: databaseHost,
|
||||
DatabasePort: databasePort,
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
CustomAccessTokenScript: &customAccessTokenScript,
|
||||
@@ -92,6 +107,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
DisableBasicAuthentication: &disableBasicAuthentication,
|
||||
DisableMagicLinkLogin: &disableMagicLinkLogin,
|
||||
DisableLoginPage: &disableLoginPage,
|
||||
DisableSignUp: &disableSignUp,
|
||||
Roles: roles,
|
||||
ProtectedRoles: protectedRoles,
|
||||
DefaultRoles: defaultRoles,
|
||||
|
60
server/resolvers/generate_jwt_keys.go
Normal file
60
server/resolvers/generate_jwt_keys.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// GenerateJWTKeysResolver mutation to generate new jwt keys
|
||||
func GenerateJWTKeysResolver(ctx context.Context, params model.GenerateJWTKeysInput) (*model.GenerateJWTKeysResponse, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return nil, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
clientID := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID)
|
||||
if crypto.IsHMACA(params.Type) {
|
||||
secret, _, err := crypto.NewHMACKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
Secret: &secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if crypto.IsRSA(params.Type) {
|
||||
_, privateKey, publicKey, _, err := crypto.NewRSAKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
PrivateKey: &privateKey,
|
||||
PublicKey: &publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if crypto.IsECDSA(params.Type) {
|
||||
_, privateKey, publicKey, _, err := crypto.NewECDSAKey(params.Type, clientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.GenerateJWTKeysResponse{
|
||||
PrivateKey: &privateKey,
|
||||
PublicKey: &publicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid algorithm")
|
||||
}
|
@@ -126,7 +126,7 @@ func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL)
|
||||
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL, redirectURL)
|
||||
}
|
||||
|
||||
return &model.Response{
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
@@ -35,6 +36,10 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
return res, fmt.Errorf(`user with this email not found`)
|
||||
}
|
||||
|
||||
if user.RevokedTimestamp != nil {
|
||||
return res, fmt.Errorf(`user access has been revoked`)
|
||||
}
|
||||
|
||||
if !strings.Contains(user.SignupMethods, constants.SignupMethodBasicAuth) {
|
||||
return res, fmt.Errorf(`user has not signed up email & password`)
|
||||
}
|
||||
@@ -69,7 +74,11 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
return res, err
|
||||
}
|
||||
|
||||
expiresIn := int64(1800)
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
res = &model.AuthResponse{
|
||||
Message: `Logged in successfully`,
|
||||
AccessToken: &authToken.AccessToken.Token,
|
||||
|
@@ -43,8 +43,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
|
||||
// find user with email
|
||||
existingUser, err := db.Provider.GetUserByEmail(params.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||
// define roles for new user
|
||||
if len(params.Roles) > 0 {
|
||||
@@ -67,6 +70,10 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
// 2. user has not signed up for one of the available role but trying to signup.
|
||||
// Need to modify roles in this case
|
||||
|
||||
if user.RevokedTimestamp != nil {
|
||||
return res, fmt.Errorf(`user access has been revoked`)
|
||||
}
|
||||
|
||||
// find the unassigned roles
|
||||
if len(params.Roles) <= 0 {
|
||||
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||
|
@@ -35,6 +35,10 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
|
||||
return res, fmt.Errorf(`passwords don't match`)
|
||||
}
|
||||
|
||||
if !utils.IsValidPassword(params.Password) {
|
||||
return res, fmt.Errorf(`password is not valid. It needs to be at least 6 characters long and contain at least one number, one uppercase letter, one lowercase letter and one special character`)
|
||||
}
|
||||
|
||||
// verify if token exists in db
|
||||
hostname := utils.GetHost(gc)
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
||||
|
49
server/resolvers/revoke_access.go
Normal file
49
server/resolvers/revoke_access.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
)
|
||||
|
||||
// RevokeAccessResolver is a resolver for revoking user access
|
||||
func RevokeAccessResolver(ctx context.Context, params model.UpdateAccessInput) (*model.Response, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
var res *model.Response
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if !token.IsSuperAdmin(gc) {
|
||||
return res, fmt.Errorf("unauthorized")
|
||||
}
|
||||
|
||||
user, err := db.Provider.GetUserByID(params.UserID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
user.RevokedTimestamp = &now
|
||||
|
||||
user, err = db.Provider.UpdateUser(user)
|
||||
if err != nil {
|
||||
log.Println("error updating user:", err)
|
||||
return res, err
|
||||
}
|
||||
|
||||
go sessionstore.DeleteAllUserSession(fmt.Sprintf("%x", user.ID))
|
||||
|
||||
res = &model.Response{
|
||||
Message: `user access revoked successfully`,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
@@ -73,7 +74,11 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
res = &model.AuthResponse{
|
||||
Message: `Session token refreshed`,
|
||||
AccessToken: &authToken.AccessToken.Token,
|
||||
|
@@ -28,13 +28,22 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
return res, err
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
||||
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
||||
}
|
||||
|
||||
if params.ConfirmPassword != params.Password {
|
||||
return res, fmt.Errorf(`password and confirm password does not match`)
|
||||
}
|
||||
|
||||
if !utils.IsValidPassword(params.Password) {
|
||||
return res, fmt.Errorf(`password is not valid. It needs to be at least 6 characters long and contain at least one number, one uppercase letter, one lowercase letter and one special character`)
|
||||
}
|
||||
|
||||
params.Email = strings.ToLower(params.Email)
|
||||
|
||||
if !utils.IsValidEmail(params.Email) {
|
||||
@@ -129,6 +138,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
}
|
||||
verificationType := constants.VerificationTypeBasicAuthSignup
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
return res, err
|
||||
@@ -164,7 +176,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
go utils.SaveSessionInDB(gc, user.ID)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
res = &model.AuthResponse{
|
||||
Message: `Signed up successfully.`,
|
||||
|
@@ -53,11 +53,19 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
}
|
||||
|
||||
if isJWTUpdated {
|
||||
// use to reset when type is changed from rsa, edsa -> hmac or vice a versa
|
||||
defaultSecret := ""
|
||||
defaultPublicKey := ""
|
||||
defaultPrivateKey := ""
|
||||
// check if jwt secret is provided
|
||||
if crypto.IsHMACA(algo) {
|
||||
if params.JwtSecret == nil {
|
||||
return res, fmt.Errorf("jwt secret is required for HMAC algorithm")
|
||||
}
|
||||
|
||||
// reset public key and private key
|
||||
params.JwtPrivateKey = &defaultPrivateKey
|
||||
params.JwtPublicKey = &defaultPublicKey
|
||||
}
|
||||
|
||||
if crypto.IsRSA(algo) {
|
||||
@@ -65,6 +73,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm")
|
||||
}
|
||||
|
||||
// reset the jwt secret
|
||||
params.JwtSecret = &defaultSecret
|
||||
_, err = crypto.ParseRsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
@@ -81,6 +91,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||
return res, fmt.Errorf("jwt private and public key is required for RSA (PKCS1) / ECDSA algorithm")
|
||||
}
|
||||
|
||||
// reset the jwt secret
|
||||
params.JwtSecret = &defaultSecret
|
||||
_, err = crypto.ParseEcdsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||
if err != nil {
|
||||
return res, err
|
||||
|
@@ -154,6 +154,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
return res, err
|
||||
}
|
||||
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
res = &model.User{
|
||||
ID: params.ID,
|
||||
Email: user.Email,
|
||||
@@ -161,8 +163,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
GivenName: user.GivenName,
|
||||
FamilyName: user.FamilyName,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
86
server/resolvers/validate_jwt_token.go
Normal file
86
server/resolvers/validate_jwt_token.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
// ValidateJwtTokenResolver is used to validate a jwt token without its rotation
|
||||
// this can be used at API level (backend)
|
||||
// it can validate:
|
||||
// access_token
|
||||
// id_token
|
||||
// refresh_token
|
||||
func ValidateJwtTokenResolver(ctx context.Context, params model.ValidateJWTTokenInput) (*model.ValidateJWTTokenResponse, error) {
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tokenType := params.TokenType
|
||||
if tokenType != "access_token" && tokenType != "refresh_token" && tokenType != "id_token" {
|
||||
return nil, errors.New("invalid token type")
|
||||
}
|
||||
|
||||
userID := ""
|
||||
nonce := ""
|
||||
// access_token and refresh_token should be validated from session store as well
|
||||
if tokenType == "access_token" || tokenType == "refresh_token" {
|
||||
savedSession := sessionstore.GetState(params.Token)
|
||||
if savedSession == "" {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
savedSessionSplit := strings.Split(savedSession, "@")
|
||||
nonce = savedSessionSplit[0]
|
||||
userID = savedSessionSplit[1]
|
||||
}
|
||||
|
||||
hostname := utils.GetHost(gc)
|
||||
var claimRoles []string
|
||||
var claims jwt.MapClaims
|
||||
|
||||
// we cannot validate sub and nonce in case of id_token as that token is not persisted in session store
|
||||
if userID != "" && nonce != "" {
|
||||
claims, err = token.ParseJWTToken(params.Token, hostname, nonce, userID)
|
||||
if err != nil {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
claims, err = token.ParseJWTTokenWithoutNonce(params.Token, hostname)
|
||||
if err != nil {
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
claimRolesInterface := claims["roles"]
|
||||
roleSlice := utils.ConvertInterfaceToSlice(claimRolesInterface)
|
||||
for _, v := range roleSlice {
|
||||
claimRoles = append(claimRoles, v.(string))
|
||||
}
|
||||
|
||||
if params.Roles != nil && len(params.Roles) > 0 {
|
||||
for _, v := range params.Roles {
|
||||
if !utils.StringSliceContains(claimRoles, v) {
|
||||
return nil, fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &model.ValidateJWTTokenResponse{
|
||||
IsValid: true,
|
||||
}, nil
|
||||
}
|
@@ -64,7 +64,11 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
go utils.SaveSessionInDB(gc, user.ID)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
expiresIn := authToken.AccessToken.ExpiresAt - time.Now().Unix()
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 1
|
||||
}
|
||||
|
||||
res = &model.AuthResponse{
|
||||
Message: `Email verified successfully.`,
|
||||
AccessToken: &authToken.AccessToken.Token,
|
||||
|
@@ -44,6 +44,7 @@ func InitRouter() *gin.Engine {
|
||||
{
|
||||
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
||||
dashboard.Static("/build", "dashboard/build")
|
||||
dashboard.Static("/public", "dashboard/public")
|
||||
dashboard.GET("/", handlers.DashboardHandler())
|
||||
dashboard.GET("/:page", handlers.DashboardHandler())
|
||||
}
|
||||
|
57
server/test/enable_access_test.go
Normal file
57
server/test/enable_access_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func enableAccessTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should enable access`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "enable_access." + s.TestInfo.Email
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
Token: verificationRequest.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, verifyRes.AccessToken)
|
||||
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
|
||||
res, err := resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
res, err = resolvers.EnableAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
// it should allow login with enabled access
|
||||
res, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
cleanData(email)
|
||||
})
|
||||
}
|
62
server/test/generate_jwt_keys_test.go
Normal file
62
server/test/generate_jwt_keys_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func generateJWTkeyTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
req, ctx := createContext(s)
|
||||
t.Run(`generate_jwt_keys`, func(t *testing.T) {
|
||||
t.Run(`should throw unauthorized`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "HS256",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
t.Run(`should throw invalid`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "test",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
})
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
t.Run(`should generate HS256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "HS256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Secret)
|
||||
})
|
||||
|
||||
t.Run(`should generate RS256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "RS256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.PrivateKey)
|
||||
assert.NotEmpty(t, res.PublicKey)
|
||||
})
|
||||
|
||||
t.Run(`should generate ES256 secret`, func(t *testing.T) {
|
||||
res, err := resolvers.GenerateJWTKeysResolver(ctx, model.GenerateJWTKeysInput{
|
||||
Type: "ES256",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.PrivateKey)
|
||||
assert.NotEmpty(t, res.PublicKey)
|
||||
})
|
||||
})
|
||||
}
|
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,11 +17,17 @@ func magicLinkLoginTests(t *testing.T, s TestSetup) {
|
||||
t.Run(`should login with magic link`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "magic_link_login." + s.TestInfo.Email
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, true)
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, err, "signup disabled")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, false)
|
||||
_, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Nil(t, err, "signup should be successful")
|
||||
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
|
@@ -43,6 +43,14 @@ func resetPasswordTest(t *testing.T, s TestSetup) {
|
||||
ConfirmPassword: "test1",
|
||||
})
|
||||
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
_, err = resolvers.ResetPasswordResolver(ctx, model.ResetPasswordInput{
|
||||
Token: verificationRequest.Token,
|
||||
Password: "Test@1234",
|
||||
ConfirmPassword: "Test@1234",
|
||||
})
|
||||
|
||||
assert.Nil(t, err, "password changed successfully")
|
||||
|
||||
cleanData(email)
|
||||
|
@@ -11,9 +11,10 @@ import (
|
||||
|
||||
func TestResolvers(t *testing.T) {
|
||||
databases := map[string]string{
|
||||
constants.DbTypeSqlite: "../../data.db",
|
||||
// constants.DbTypeSqlite: "../../data.db",
|
||||
// constants.DbTypeArangodb: "http://localhost:8529",
|
||||
// constants.DbTypeMongodb: "mongodb://localhost:27017",
|
||||
constants.DbTypeCassandraDB: "127.0.0.1:9042",
|
||||
}
|
||||
|
||||
for dbType, dbURL := range databases {
|
||||
@@ -48,6 +49,9 @@ func TestResolvers(t *testing.T) {
|
||||
adminSessionTests(t, s)
|
||||
updateEnvTests(t, s)
|
||||
envTests(t, s)
|
||||
revokeAccessTest(t, s)
|
||||
enableAccessTest(t, s)
|
||||
generateJWTkeyTest(t, s)
|
||||
|
||||
// user tests
|
||||
loginTests(t, s)
|
||||
@@ -63,6 +67,7 @@ func TestResolvers(t *testing.T) {
|
||||
logoutTests(t, s)
|
||||
metaTests(t, s)
|
||||
inviteUserTest(t, s)
|
||||
validateJwtTokenTest(t, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
54
server/test/revoke_access_test.go
Normal file
54
server/test/revoke_access_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func revokeAccessTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
t.Run(`should revoke access`, func(t *testing.T) {
|
||||
req, ctx := createContext(s)
|
||||
email := "revoke_access." + s.TestInfo.Email
|
||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||
Token: verificationRequest.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, verifyRes.AccessToken)
|
||||
|
||||
res, err := resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
h, err := crypto.EncryptPassword(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||
assert.Nil(t, err)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||
|
||||
res, err = resolvers.RevokeAccessResolver(ctx, model.UpdateAccessInput{
|
||||
UserID: verifyRes.User.ID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, res.Message)
|
||||
|
||||
// it should not allow login with revoked access
|
||||
_, err = resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||
Email: email,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
cleanData(email)
|
||||
})
|
||||
}
|
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -20,14 +21,30 @@ func signupTests(t *testing.T, s TestSetup) {
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password + "s",
|
||||
})
|
||||
assert.NotNil(t, err, "invalid password errors")
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: "test",
|
||||
ConfirmPassword: "test",
|
||||
})
|
||||
assert.NotNil(t, err, "invalid password")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, true)
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password,
|
||||
})
|
||||
assert.NotNil(t, err, "singup disabled")
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, false)
|
||||
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||
Email: email,
|
||||
Password: s.TestInfo.Password,
|
||||
ConfirmPassword: s.TestInfo.Password,
|
||||
})
|
||||
assert.Nil(t, err, "signup should be successful")
|
||||
user := *res.User
|
||||
assert.Equal(t, email, user.Email)
|
||||
assert.Nil(t, res.AccessToken, "access token should be nil")
|
||||
|
@@ -31,26 +31,31 @@ type TestSetup struct {
|
||||
}
|
||||
|
||||
func cleanData(email string) {
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeBasicAuthSignup)
|
||||
if err == nil {
|
||||
err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
}
|
||||
// verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeBasicAuthSignup)
|
||||
// if err == nil {
|
||||
// err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
// }
|
||||
|
||||
verificationRequest, err = db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeForgotPassword)
|
||||
if err == nil {
|
||||
err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
}
|
||||
// verificationRequest, err = db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeForgotPassword)
|
||||
// if err == nil {
|
||||
// err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
// }
|
||||
|
||||
verificationRequest, err = db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeUpdateEmail)
|
||||
if err == nil {
|
||||
err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
}
|
||||
// verificationRequest, err = db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeUpdateEmail)
|
||||
// if err == nil {
|
||||
// err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
// }
|
||||
|
||||
dbUser, err := db.Provider.GetUserByEmail(email)
|
||||
if err == nil {
|
||||
db.Provider.DeleteUser(dbUser)
|
||||
db.Provider.DeleteSession(dbUser.ID)
|
||||
}
|
||||
// verificationRequest, err = db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||
// if err == nil {
|
||||
// err = db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
// }
|
||||
|
||||
// dbUser, err := db.Provider.GetUserByEmail(email)
|
||||
// if err == nil {
|
||||
// db.Provider.DeleteUser(dbUser)
|
||||
// db.Provider.DeleteSession(dbUser.ID)
|
||||
// }
|
||||
}
|
||||
|
||||
func createContext(s TestSetup) (*http.Request, context.Context) {
|
||||
@@ -68,7 +73,7 @@ func createContext(s TestSetup) (*http.Request, context.Context) {
|
||||
func testSetup() TestSetup {
|
||||
testData := TestData{
|
||||
Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()),
|
||||
Password: "test",
|
||||
Password: "Test@123",
|
||||
}
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, "../../.env.sample")
|
||||
|
90
server/test/validate_jwt_token_test.go
Normal file
90
server/test/validate_jwt_token_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func validateJwtTokenTest(t *testing.T, s TestSetup) {
|
||||
t.Helper()
|
||||
_, ctx := createContext(s)
|
||||
t.Run(`validate params`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: "",
|
||||
})
|
||||
assert.False(t, res.IsValid)
|
||||
res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: "invalid",
|
||||
})
|
||||
assert.False(t, res.IsValid)
|
||||
_, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token_invalid",
|
||||
Token: "invalid@invalid",
|
||||
})
|
||||
assert.Error(t, err, "invalid token")
|
||||
})
|
||||
|
||||
scope := []string{"openid", "email", "profile", "offline_access"}
|
||||
user := models.User{
|
||||
ID: uuid.New().String(),
|
||||
Email: "jwt_test_" + s.TestInfo.Email,
|
||||
Roles: "user",
|
||||
UpdatedAt: time.Now().Unix(),
|
||||
CreatedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
roles := []string{"user"}
|
||||
gc, err := utils.GinContextFromContext(ctx)
|
||||
assert.NoError(t, err)
|
||||
authToken, err := token.CreateAuthToken(gc, user, roles, scope)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
|
||||
t.Run(`should validate the access token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: authToken.AccessToken.Token,
|
||||
Roles: []string{"user"},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
|
||||
res, err = resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "access_token",
|
||||
Token: authToken.AccessToken.Token,
|
||||
Roles: []string{"invalid_role"},
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run(`should validate the refresh token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "refresh_token",
|
||||
Token: authToken.RefreshToken.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
})
|
||||
|
||||
t.Run(`should validate the id token`, func(t *testing.T) {
|
||||
res, err := resolvers.ValidateJwtTokenResolver(ctx, model.ValidateJWTTokenInput{
|
||||
TokenType: "id_token",
|
||||
Token: authToken.IDToken.Token,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, res.IsValid)
|
||||
})
|
||||
}
|
@@ -41,3 +41,11 @@ func TestIsValidIdentifier(t *testing.T) {
|
||||
assert.True(t, utils.IsValidVerificationIdentifier(constants.VerificationTypeUpdateEmail), "it should be valid identifier")
|
||||
assert.True(t, utils.IsValidVerificationIdentifier(constants.VerificationTypeForgotPassword), "it should be valid identifier")
|
||||
}
|
||||
|
||||
func TestIsValidPassword(t *testing.T) {
|
||||
assert.False(t, utils.IsValidPassword("test"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("Te@1"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("n*rp7GGTd29V{xx%{pDb@7n{](SD.!+.Mp#*$EHDGk&$pAMf7e#432Sg,Gr](j3n]jV/3F8BJJT+9u9{q=8zK:8u!rpQBaXJp%A+7r!jQj)M(vC$UX,h;;WKm$U6i#7dBnC&2ryKzKd+(y&=Ud)hErT/j;v3t..CM).8nS)9qLtV7pmP;@2QuzDyGfL7KB()k:BpjAGL@bxD%r5gcBfh7$&wutk!wzMfPFY#nkjjqyZbEHku,{jc;gvbYq2)3w=KExnYz9Vbv:;*;?f##faxkULdMpmm&yEfePixzx+[{[38zGN;3TzF;6M#Xy_tMtx:yK*n$bc(bPyGz%EYkC&]ttUF@#aZ%$QZ:u!icF@+"), "it should be invalid password")
|
||||
assert.False(t, utils.IsValidPassword("test@123"), "it should be invalid password")
|
||||
assert.True(t, utils.IsValidPassword("Test@123"), "it should be valid password")
|
||||
}
|
||||
|
@@ -130,7 +130,11 @@ func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonc
|
||||
// CreateAccessToken util to create JWT token, based on
|
||||
// user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT
|
||||
func CreateAccessToken(user models.User, roles, scopes []string, hostName, nonce string) (string, int64, error) {
|
||||
expiryBound := time.Minute * 30
|
||||
expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime))
|
||||
if err != nil {
|
||||
expiryBound = time.Minute * 30
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(expiryBound).Unix()
|
||||
|
||||
customClaims := jwt.MapClaims{
|
||||
@@ -161,7 +165,12 @@ func GetAccessToken(gc *gin.Context) (string, error) {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
authSplit := strings.Split(auth, " ")
|
||||
if len(authSplit) != 2 {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if strings.ToLower(authSplit[0]) != "bearer" {
|
||||
return "", fmt.Errorf(`not a bearer token`)
|
||||
}
|
||||
|
||||
@@ -277,7 +286,11 @@ func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionD
|
||||
// CreateIDToken util to create JWT token, based on
|
||||
// user information, roles config and CUSTOM_ACCESS_TOKEN_SCRIPT
|
||||
func CreateIDToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) {
|
||||
expiryBound := time.Minute * 30
|
||||
expiryBound, err := utils.ParseDurationInSeconds(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAccessTokenExpiryTime))
|
||||
if err != nil {
|
||||
expiryBound = time.Minute * 30
|
||||
}
|
||||
|
||||
expiresAt := time.Now().Add(expiryBound).Unix()
|
||||
|
||||
resUser := user.AsAPIUser()
|
||||
@@ -350,7 +363,12 @@ func GetIDToken(gc *gin.Context) (string, error) {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
authSplit := strings.Split(auth, " ")
|
||||
if len(authSplit) != 2 {
|
||||
return "", fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
if strings.ToLower(authSplit[0]) != "bearer" {
|
||||
return "", fmt.Errorf(`not a bearer token`)
|
||||
}
|
||||
|
||||
|
@@ -105,3 +105,59 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// ParseJWTTokenWithoutNonce common util to parse jwt token without nonce
|
||||
// used to validate ID token as it is not persisted in store
|
||||
func ParseJWTTokenWithoutNonce(token, hostname string) (jwt.MapClaims, error) {
|
||||
jwtType := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)
|
||||
signingMethod := jwt.GetSigningMethod(jwtType)
|
||||
|
||||
var err error
|
||||
var claims jwt.MapClaims
|
||||
|
||||
switch signingMethod {
|
||||
case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)), nil
|
||||
})
|
||||
case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
key, err := crypto.ParseRsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
case jwt.SigningMethodES256, jwt.SigningMethodES384, jwt.SigningMethodES512:
|
||||
_, err = jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
key, err := crypto.ParseEcdsaPublicKeyFromPemStr(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtPublicKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return key, nil
|
||||
})
|
||||
default:
|
||||
err = errors.New("unsupported signing method")
|
||||
}
|
||||
if err != nil {
|
||||
return claims, err
|
||||
}
|
||||
|
||||
// claim parses exp & iat into float 64 with e^10,
|
||||
// but we expect it to be int64
|
||||
// hence we need to assert interface and convert to int64
|
||||
intExp := int64(claims["exp"].(float64))
|
||||
intIat := int64(claims["iat"].(float64))
|
||||
claims["exp"] = intExp
|
||||
claims["iat"] = intIat
|
||||
|
||||
if claims["aud"] != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
|
||||
return claims, errors.New("invalid audience")
|
||||
}
|
||||
|
||||
if claims["iss"] != hostname {
|
||||
return claims, errors.New("invalid issuer")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package utils
|
||||
|
||||
import (
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
"github.com/authorizerdev/authorizer/server/db/models"
|
||||
@@ -47,3 +48,24 @@ func RemoveDuplicateString(strSlice []string) []string {
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// ConvertInterfaceToSlice to convert interface to slice interface
|
||||
func ConvertInterfaceToSlice(slice interface{}) []interface{} {
|
||||
s := reflect.ValueOf(slice)
|
||||
if s.Kind() != reflect.Slice {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keep the distinction between nil and empty slice input
|
||||
if s.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make([]interface{}, s.Len())
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
ret[i] = s.Index(i).Interface()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
@@ -17,5 +17,6 @@ func GetMetaInfo() model.Meta {
|
||||
IsBasicAuthenticationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication),
|
||||
IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification),
|
||||
IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin),
|
||||
IsSignUpEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp),
|
||||
}
|
||||
}
|
||||
|
21
server/utils/parser.go
Normal file
21
server/utils/parser.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ParseDurationInSeconds parses input s, removes ms/us/ns and returns result duration
|
||||
func ParseDurationInSeconds(s string) (time.Duration, error) {
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
d = d.Truncate(time.Second)
|
||||
if d <= 0 {
|
||||
return 0, errors.New(`duration must be greater than 0s`)
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
@@ -10,7 +10,20 @@ import (
|
||||
)
|
||||
|
||||
// GetHost returns hostname from request context
|
||||
// if X-Authorizer-URL header is set it is given highest priority
|
||||
// if EnvKeyAuthorizerURL is set it is given second highest priority.
|
||||
// if above 2 are not set the requesting host name is used
|
||||
func GetHost(c *gin.Context) string {
|
||||
authorizerURL := c.Request.Header.Get("X-Authorizer-URL")
|
||||
if authorizerURL != "" {
|
||||
return authorizerURL
|
||||
}
|
||||
|
||||
authorizerURL = envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL)
|
||||
if authorizerURL != "" {
|
||||
return authorizerURL
|
||||
}
|
||||
|
||||
scheme := c.Request.Header.Get("X-Forwarded-Proto")
|
||||
if scheme != "https" {
|
||||
scheme = "http"
|
||||
|
@@ -86,3 +86,35 @@ func IsStringArrayEqual(a, b []string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidatePassword to validate the password against the following policy
|
||||
// min char length: 6
|
||||
// max char length: 36
|
||||
// at least one upper case letter
|
||||
// at least one lower case letter
|
||||
// at least one digit
|
||||
// at least one special character
|
||||
func IsValidPassword(password string) bool {
|
||||
if len(password) < 6 || len(password) > 36 {
|
||||
return false
|
||||
}
|
||||
|
||||
hasUpperCase := false
|
||||
hasLowerCase := false
|
||||
hasDigit := false
|
||||
hasSpecialChar := false
|
||||
|
||||
for _, char := range password {
|
||||
if char >= 'A' && char <= 'Z' {
|
||||
hasUpperCase = true
|
||||
} else if char >= 'a' && char <= 'z' {
|
||||
hasLowerCase = true
|
||||
} else if char >= '0' && char <= '9' {
|
||||
hasDigit = true
|
||||
} else {
|
||||
hasSpecialChar = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar
|
||||
}
|
||||
|
Reference in New Issue
Block a user