Merge branch 'authorizerdev/authorizer:main' into main
This commit is contained in:
commit
819dd57377
15
.github/workflows/release.yaml
vendored
15
.github/workflows/release.yaml
vendored
|
@ -1,4 +1,19 @@
|
||||||
on:
|
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:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -12,3 +12,5 @@ data.db
|
||||||
.env.local
|
.env.local
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.yalc
|
||||||
|
yalc.lock
|
76
README.md
76
README.md
|
@ -59,35 +59,42 @@
|
||||||
|
|
||||||
# Getting Started
|
# 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.
|
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 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
|
#### Prerequisites
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- OS: Linux or macOS or windows
|
- OS: Linux or macOS or windows
|
||||||
- Go: (Golang)(https://golang.org/dl/) >= v1.15
|
- 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**)
|
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
|
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`
|
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/)
|
4. 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`
|
5. Build Dashboard `make build-dashboard`
|
||||||
7. Build App `make build-app`
|
6. Build App `make build-app`
|
||||||
8. Build Server `make clean && make`
|
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
|
> 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)
|
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:
|
binaries are baked with required deployment files and bundled. You can download a specific version of it for the following operating systems:
|
||||||
|
@ -95,7 +102,7 @@ binaries are baked with required deployment files and bundled. You can download
|
||||||
- Mac OSX
|
- Mac OSX
|
||||||
- Linux
|
- 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)
|
- Download the Bundle for the specific OS from the [release page](https://github.com/authorizerdev/authorizer/releases)
|
||||||
|
|
||||||
|
@ -115,11 +122,7 @@ binaries are baked with required deployment files and bundled. You can download
|
||||||
cd authorizer
|
cd authorizer
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Configure environment variables
|
#### Step 3: Start Authorizer
|
||||||
|
|
||||||
Required environment variables are pre-configured in `.env` file. But based on the production requirements, please configure more environment variables. You can refer to [environment variables docs](/core/env) for more information.
|
|
||||||
|
|
||||||
### Step 3: Start Authorizer
|
|
||||||
|
|
||||||
- Run following command to start authorizer
|
- Run following command to start authorizer
|
||||||
|
|
||||||
|
@ -131,20 +134,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`
|
> 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** |
|
- Open authorizer instance endpoint in browser
|
||||||
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
|
- Sign up as an admin with a secure password
|
||||||
| 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) |
|
- Configure environment variables from authorizer dashboard. Check env [docs](/core/env) for more information
|
||||||
| 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) |
|
> Note: `DATABASE_URL`, `DATABASE_TYPE` and `DATABASE_NAME` are only configurable via platform envs
|
||||||
|
|
||||||
### Things to consider
|
### Things to consider
|
||||||
|
|
||||||
- For social logins, you will need respective social platform key and secret
|
- For social logins, you will need respective social platform key and secret
|
||||||
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
|
- For having verified users, you will need an SMTP server with an email address and password using which system can send emails. The system will send a verification link to an email address. Once an email is verified then, only able to access it.
|
||||||
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
|
> Note: One can always disable the email verification to allow open sign up, which is not recommended for production as anyone can use anyone's email address 😅
|
||||||
- For persisting user sessions, you will need Redis URL (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
|
## Testing
|
||||||
|
|
||||||
|
@ -163,8 +166,9 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const authorizerRef = new authorizerdev.Authorizer({
|
const authorizerRef = new authorizerdev.Authorizer({
|
||||||
authorizerURL: `AUTHORIZER_URL`,
|
authorizerURL: `YOUR_AUTHORIZER_INSTANCE_URL`,
|
||||||
redirectURL: window.location.origin,
|
redirectURL: window.location.origin,
|
||||||
|
clientID: 'YOUR_CLIENT_ID', // obtain your client id from authorizer dashboard
|
||||||
});
|
});
|
||||||
|
|
||||||
// use the button selector as per your application
|
// use the button selector as per your application
|
||||||
|
@ -175,15 +179,19 @@ This example demonstrates how you can use [`@authorizerdev/authorizer-js`](/auth
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onLoad() {
|
async function onLoad() {
|
||||||
const res = await authorizerRef.browserLogin();
|
const res = await authorizerRef.authorize({
|
||||||
if (res && res.user) {
|
response_type: 'code',
|
||||||
|
use_refresh_token: false,
|
||||||
|
});
|
||||||
|
if (res && res.access_token) {
|
||||||
// you can use user information here, eg:
|
// you can use user information here, eg:
|
||||||
/**
|
const user = await authorizerRef.getProfile({
|
||||||
|
Authorization: `Bearer ${res.access_token}`,
|
||||||
|
});
|
||||||
const userSection = document.getElementById('user');
|
const userSection = document.getElementById('user');
|
||||||
const logoutSection = document.getElementById('logout-section');
|
const logoutSection = document.getElementById('logout-section');
|
||||||
logoutSection.classList.toggle('hide');
|
logoutSection.classList.toggle('hide');
|
||||||
userSection.innerHTML = `Welcome, ${res.user.email}`;
|
userSection.innerHTML = `Welcome, ${user.email}`;
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onLoad();
|
onLoad();
|
||||||
|
|
28
app/package-lock.json
generated
28
app/package-lock.json
generated
|
@ -24,9 +24,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@authorizerdev/authorizer-js": {
|
"node_modules/@authorizerdev/authorizer-js": {
|
||||||
"version": "0.4.0-beta.3",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz",
|
||||||
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
"integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
},
|
},
|
||||||
|
@ -35,11 +35,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@authorizerdev/authorizer-react": {
|
"node_modules/@authorizerdev/authorizer-react": {
|
||||||
"version": "0.9.0-beta.7",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz",
|
||||||
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
"integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
|
"@authorizerdev/authorizer-js": "^0.6.0",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"react-final-form": "^6.5.3",
|
"react-final-form": "^6.5.3",
|
||||||
"styled-components": "^5.3.0"
|
"styled-components": "^5.3.0"
|
||||||
|
@ -829,19 +829,19 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@authorizerdev/authorizer-js": {
|
"@authorizerdev/authorizer-js": {
|
||||||
"version": "0.4.0-beta.3",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.4.0-beta.3.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz",
|
||||||
"integrity": "sha512-OGZc6I6cnpi/WkSotkjVIc3LEzl8pFeiohr8+Db9xWd75/oTfOZqWRuIHTnTc1FC+6Sv2EjTJ9Aa6lrloWG+NQ==",
|
"integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-fetch": "^2.6.1"
|
"node-fetch": "^2.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@authorizerdev/authorizer-react": {
|
"@authorizerdev/authorizer-react": {
|
||||||
"version": "0.9.0-beta.7",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.9.0-beta.7.tgz",
|
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz",
|
||||||
"integrity": "sha512-hCGsVionKMZNk+uD0CLtMIkUzhQqpHbVntko3rY+O7ouOrTrikY/WQVPbo1bqX1cu/6/cHE4RVU3cZ7V5xnxVg==",
|
"integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@authorizerdev/authorizer-js": "^0.4.0-beta.3",
|
"@authorizerdev/authorizer-js": "^0.6.0",
|
||||||
"final-form": "^4.20.2",
|
"final-form": "^4.20.2",
|
||||||
"react-final-form": "^6.5.3",
|
"react-final-form": "^6.5.3",
|
||||||
"styled-components": "^5.3.0"
|
"styled-components": "^5.3.0"
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function App() {
|
||||||
if (redirectURL) {
|
if (redirectURL) {
|
||||||
urlProps.redirectURL = redirectURL;
|
urlProps.redirectURL = redirectURL;
|
||||||
} else {
|
} else {
|
||||||
urlProps.redirectURL = window.location.origin;
|
urlProps.redirectURL = window.location.origin + '/app';
|
||||||
}
|
}
|
||||||
const globalState: Record<string, string> = {
|
const globalState: Record<string, string> = {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect, lazy, Suspense } from 'react';
|
import React, { useEffect, lazy, Suspense } from 'react';
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
||||||
|
import SetupPassword from './pages/setup-password';
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
||||||
const Login = lazy(() => import('./pages/login'));
|
const Login = lazy(() => import('./pages/login'));
|
||||||
|
@ -60,6 +61,9 @@ export default function Root({
|
||||||
<Route path="/app/reset-password">
|
<Route path="/app/reset-password">
|
||||||
<ResetPassword />
|
<ResetPassword />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/app/setup-password">
|
||||||
|
<SetupPassword />
|
||||||
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|
12
app/src/pages/setup-password.tsx
Normal file
12
app/src/pages/setup-password.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React, { Fragment } from 'react';
|
||||||
|
import { AuthorizerResetPassword } from '@authorizerdev/authorizer-react';
|
||||||
|
|
||||||
|
export default function SetupPassword() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<h1 style={{ textAlign: 'center' }}>Setup new Password</h1>
|
||||||
|
<br />
|
||||||
|
<AuthorizerResetPassword />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
71
dashboard/package-lock.json
generated
71
dashboard/package-lock.json
generated
|
@ -22,6 +22,7 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-dropzone": "^12.0.4",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^4.5.4",
|
||||||
|
@ -1251,6 +1252,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
"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": {
|
"node_modules/babel-plugin-macros": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
"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"
|
"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": {
|
"node_modules/find-root": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||||
|
@ -1914,9 +1934,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.0",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
@ -1959,6 +1979,22 @@
|
||||||
"react": "17.0.2"
|
"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": {
|
"node_modules/react-fast-compare": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
"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": {
|
"babel-plugin-macros": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
"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": {
|
"find-root": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||||
|
@ -3707,9 +3756,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prop-types": {
|
"prop-types": {
|
||||||
"version": "15.8.0",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
@ -3743,6 +3792,16 @@
|
||||||
"scheduler": "^0.20.2"
|
"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": {
|
"react-fast-compare": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-dropzone": "^12.0.4",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"typescript": "^4.5.4",
|
"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
|
|
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;
|
|
@ -61,6 +61,7 @@ export const SwitchInputType = {
|
||||||
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
|
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
|
||||||
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
|
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
|
||||||
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
|
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
|
||||||
|
DISABLE_SIGN_UP: 'DISABLE_SIGN_UP',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DateInputType = {
|
export const DateInputType = {
|
||||||
|
@ -89,3 +90,41 @@ export const ECDSAEncryptionType = {
|
||||||
ES384: 'ES384',
|
ES384: 'ES384',
|
||||||
ES512: 'ES512',
|
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,6 +48,7 @@ export const EnvVariablesQuery = `
|
||||||
DISABLE_MAGIC_LINK_LOGIN,
|
DISABLE_MAGIC_LINK_LOGIN,
|
||||||
DISABLE_EMAIL_VERIFICATION,
|
DISABLE_EMAIL_VERIFICATION,
|
||||||
DISABLE_BASIC_AUTHENTICATION,
|
DISABLE_BASIC_AUTHENTICATION,
|
||||||
|
DISABLE_SIGN_UP,
|
||||||
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||||
DATABASE_NAME,
|
DATABASE_NAME,
|
||||||
DATABASE_TYPE,
|
DATABASE_TYPE,
|
||||||
|
@ -81,7 +82,16 @@ export const UserDetailsQuery = `
|
||||||
signup_methods
|
signup_methods
|
||||||
roles
|
roles
|
||||||
created_at
|
created_at
|
||||||
|
revoked_timestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const EmailVerificationQuery = `
|
||||||
|
query {
|
||||||
|
_env{
|
||||||
|
DISABLE_EMAIL_VERIFICATION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -34,46 +34,11 @@ import {
|
||||||
HMACEncryptionType,
|
HMACEncryptionType,
|
||||||
RSAEncryptionType,
|
RSAEncryptionType,
|
||||||
ECDSAEncryptionType,
|
ECDSAEncryptionType,
|
||||||
|
envVarTypes,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { UpdateEnvVariables } from '../graphql/mutation';
|
import { UpdateEnvVariables } from '../graphql/mutation';
|
||||||
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
||||||
|
import GenerateKeysModal from '../components/GenerateKeysModal';
|
||||||
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;
|
|
||||||
ACCESS_TOKEN_EXPIRY_TIME: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Environment() {
|
export default function Environment() {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
|
@ -115,6 +80,7 @@ export default function Environment() {
|
||||||
DISABLE_MAGIC_LINK_LOGIN: false,
|
DISABLE_MAGIC_LINK_LOGIN: false,
|
||||||
DISABLE_EMAIL_VERIFICATION: false,
|
DISABLE_EMAIL_VERIFICATION: false,
|
||||||
DISABLE_BASIC_AUTHENTICATION: false,
|
DISABLE_BASIC_AUTHENTICATION: false,
|
||||||
|
DISABLE_SIGN_UP: false,
|
||||||
OLD_ADMIN_SECRET: '',
|
OLD_ADMIN_SECRET: '',
|
||||||
DATABASE_NAME: '',
|
DATABASE_NAME: '',
|
||||||
DATABASE_TYPE: '',
|
DATABASE_TYPE: '',
|
||||||
|
@ -134,14 +100,10 @@ export default function Environment() {
|
||||||
OLD_ADMIN_SECRET: false,
|
OLD_ADMIN_SECRET: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
async function getData() {
|
async function getData() {
|
||||||
const {
|
const {
|
||||||
data: { _env: envData },
|
data: { _env: envData },
|
||||||
} = await client.query(EnvVariablesQuery).toPromise();
|
} = await client.query(EnvVariablesQuery).toPromise();
|
||||||
|
|
||||||
if (isMounted) {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setEnvVariables({
|
setEnvVariables({
|
||||||
...envData,
|
...envData,
|
||||||
|
@ -153,13 +115,9 @@ export default function Environment() {
|
||||||
disableInputField: true,
|
disableInputField: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
getData();
|
getData();
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const validateAdminSecretHandler = (event: any) => {
|
const validateAdminSecretHandler = (event: any) => {
|
||||||
|
@ -230,6 +188,8 @@ export default function Environment() {
|
||||||
disableInputField: true,
|
disableInputField: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getData();
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: `Successfully updated ${
|
title: `Successfully updated ${
|
||||||
Object.keys(updatedEnvVariables).length
|
Object.keys(updatedEnvVariables).length
|
||||||
|
@ -256,7 +216,7 @@ export default function Environment() {
|
||||||
setVariables={() => {}}
|
setVariables={() => {}}
|
||||||
inputType={TextInputType.CLIENT_ID}
|
inputType={TextInputType.CLIENT_ID}
|
||||||
placeholder="Client ID"
|
placeholder="Client ID"
|
||||||
isDisabled={true}
|
readOnly={true}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -272,7 +232,7 @@ export default function Environment() {
|
||||||
setFieldVisibility={setFieldVisibility}
|
setFieldVisibility={setFieldVisibility}
|
||||||
inputType={HiddenInputType.CLIENT_SECRET}
|
inputType={HiddenInputType.CLIENT_SECRET}
|
||||||
placeholder="Client Secret"
|
placeholder="Client Secret"
|
||||||
isDisabled={true}
|
readOnly={true}
|
||||||
/>
|
/>
|
||||||
</Center>
|
</Center>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -410,9 +370,22 @@ export default function Environment() {
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Divider marginTop="2%" marginBottom="2%" />
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
<Flex
|
||||||
|
width="100%"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
paddingTop="2%"
|
||||||
|
>
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
JWT (JSON Web Tokens) Configurations
|
JWT (JSON Web Tokens) Configurations
|
||||||
</Text>
|
</Text>
|
||||||
|
<Flex>
|
||||||
|
<GenerateKeysModal
|
||||||
|
jwtType={envVariables.JWT_TYPE}
|
||||||
|
getData={getData}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
<Stack spacing={6} padding="2% 0%">
|
<Stack spacing={6} padding="2% 0%">
|
||||||
<Flex>
|
<Flex>
|
||||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
@ -712,6 +685,18 @@ export default function Environment() {
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
</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>
|
</Stack>
|
||||||
<Divider marginTop="2%" marginBottom="2%" />
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
|
|
@ -38,10 +38,11 @@ import {
|
||||||
FaExclamationCircle,
|
FaExclamationCircle,
|
||||||
FaAngleDown,
|
FaAngleDown,
|
||||||
} from 'react-icons/fa';
|
} from 'react-icons/fa';
|
||||||
import { UserDetailsQuery } from '../graphql/queries';
|
import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries';
|
||||||
import { UpdateUser } from '../graphql/mutation';
|
import { EnableAccess, RevokeAccess, UpdateUser } from '../graphql/mutation';
|
||||||
import EditUserModal from '../components/EditUserModal';
|
import EditUserModal from '../components/EditUserModal';
|
||||||
import DeleteUserModal from '../components/DeleteUserModal';
|
import DeleteUserModal from '../components/DeleteUserModal';
|
||||||
|
import InviteMembersModal from '../components/InviteMembersModal';
|
||||||
|
|
||||||
interface paginationPropTypes {
|
interface paginationPropTypes {
|
||||||
limit: number;
|
limit: number;
|
||||||
|
@ -66,6 +67,12 @@ interface userDataTypes {
|
||||||
signup_methods: string;
|
signup_methods: string;
|
||||||
roles: [string];
|
roles: [string];
|
||||||
created_at: number;
|
created_at: number;
|
||||||
|
revoked_timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enum updateAccessActions {
|
||||||
|
REVOKE = 'REVOKE',
|
||||||
|
ENABLE = 'ENABLE',
|
||||||
}
|
}
|
||||||
|
|
||||||
const getMaxPages = (pagination: paginationPropTypes) => {
|
const getMaxPages = (pagination: paginationPropTypes) => {
|
||||||
|
@ -101,6 +108,8 @@ export default function Users() {
|
||||||
});
|
});
|
||||||
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
|
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
|
||||||
const [loading, setLoading] = React.useState<boolean>(false);
|
const [loading, setLoading] = React.useState<boolean>(false);
|
||||||
|
const [disableInviteMembers, setDisableInviteMembers] =
|
||||||
|
React.useState<boolean>(true);
|
||||||
const updateUserList = async () => {
|
const updateUserList = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { data } = await client
|
const { data } = await client
|
||||||
|
@ -132,8 +141,18 @@ export default function Users() {
|
||||||
}
|
}
|
||||||
setLoading(false);
|
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(() => {
|
React.useEffect(() => {
|
||||||
updateUserList();
|
updateUserList();
|
||||||
|
checkEmailVerification();
|
||||||
}, []);
|
}, []);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
updateUserList();
|
updateUserList();
|
||||||
|
@ -171,12 +190,77 @@ export default function Users() {
|
||||||
}
|
}
|
||||||
updateUserList();
|
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 (
|
return (
|
||||||
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
||||||
<Text fontSize="md" fontWeight="bold">
|
<Text fontSize="md" fontWeight="bold">
|
||||||
Users
|
Users
|
||||||
</Text>
|
</Text>
|
||||||
|
<InviteMembersModal
|
||||||
|
disabled={disableInviteMembers}
|
||||||
|
updateUserList={updateUserList}
|
||||||
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
{!loading ? (
|
{!loading ? (
|
||||||
userList.length > 0 ? (
|
userList.length > 0 ? (
|
||||||
|
@ -188,6 +272,7 @@ export default function Users() {
|
||||||
<Th>Signup Methods</Th>
|
<Th>Signup Methods</Th>
|
||||||
<Th>Roles</Th>
|
<Th>Roles</Th>
|
||||||
<Th>Verified</Th>
|
<Th>Verified</Th>
|
||||||
|
<Th>Access</Th>
|
||||||
<Th>Actions</Th>
|
<Th>Actions</Th>
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
|
@ -196,7 +281,7 @@ export default function Users() {
|
||||||
const { email_verified, created_at, ...rest }: any = user;
|
const { email_verified, created_at, ...rest }: any = user;
|
||||||
return (
|
return (
|
||||||
<Tr key={user.id} style={{ fontSize: 14 }}>
|
<Tr key={user.id} style={{ fontSize: 14 }}>
|
||||||
<Td>{user.email}</Td>
|
<Td maxW="300">{user.email}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
|
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
|
||||||
</Td>
|
</Td>
|
||||||
|
@ -211,6 +296,15 @@ export default function Users() {
|
||||||
{user.email_verified.toString()}
|
{user.email_verified.toString()}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Td>
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={user.revoked_timestamp ? 'red' : 'green'}
|
||||||
|
>
|
||||||
|
{user.revoked_timestamp ? 'Revoked' : 'Enabled'}
|
||||||
|
</Tag>
|
||||||
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={Button} variant="unstyled" size="sm">
|
<MenuButton as={Button} variant="unstyled" size="sm">
|
||||||
|
@ -240,6 +334,29 @@ export default function Users() {
|
||||||
user={rest}
|
user={rest}
|
||||||
updateUserList={updateUserList}
|
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>
|
</MenuList>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Td>
|
</Td>
|
||||||
|
|
|
@ -64,3 +64,25 @@ export const getObjectDiff = (obj1: any, obj2: any) => {
|
||||||
|
|
||||||
return diff;
|
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;
|
|
@ -69,6 +69,8 @@ const (
|
||||||
EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN"
|
EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN"
|
||||||
// EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE
|
// EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE
|
||||||
EnvKeyDisableLoginPage = "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 key for env variable ROLES
|
||||||
EnvKeyRoles = "ROLES"
|
EnvKeyRoles = "ROLES"
|
||||||
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
||||||
|
|
|
@ -27,11 +27,16 @@ type User struct {
|
||||||
Roles string `json:"roles" bson:"roles"`
|
Roles string `json:"roles" bson:"roles"`
|
||||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
|
RevokedTimestamp *int64 `json:"revoked_timestamp" bson:"revoked_timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) AsAPIUser() *model.User {
|
func (user *User) AsAPIUser() *model.User {
|
||||||
isEmailVerified := user.EmailVerifiedAt != nil
|
isEmailVerified := user.EmailVerifiedAt != nil
|
||||||
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
||||||
|
email := user.Email
|
||||||
|
createdAt := user.CreatedAt
|
||||||
|
updatedAt := user.UpdatedAt
|
||||||
|
revokedTimestamp := user.RevokedTimestamp
|
||||||
return &model.User{
|
return &model.User{
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
|
@ -41,14 +46,15 @@ func (user *User) AsAPIUser() *model.User {
|
||||||
FamilyName: user.FamilyName,
|
FamilyName: user.FamilyName,
|
||||||
MiddleName: user.MiddleName,
|
MiddleName: user.MiddleName,
|
||||||
Nickname: user.Nickname,
|
Nickname: user.Nickname,
|
||||||
PreferredUsername: &user.Email,
|
PreferredUsername: &email,
|
||||||
Gender: user.Gender,
|
Gender: user.Gender,
|
||||||
Birthdate: user.Birthdate,
|
Birthdate: user.Birthdate,
|
||||||
PhoneNumber: user.PhoneNumber,
|
PhoneNumber: user.PhoneNumber,
|
||||||
PhoneNumberVerified: &isPhoneVerified,
|
PhoneNumberVerified: &isPhoneVerified,
|
||||||
Picture: user.Picture,
|
Picture: user.Picture,
|
||||||
Roles: strings.Split(user.Roles, ","),
|
Roles: strings.Split(user.Roles, ","),
|
||||||
CreatedAt: &user.CreatedAt,
|
CreatedAt: &createdAt,
|
||||||
UpdatedAt: &user.UpdatedAt,
|
UpdatedAt: &updatedAt,
|
||||||
|
RevokedTimestamp: revokedTimestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,23 @@ type VerificationRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
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{
|
return &model.VerificationRequest{
|
||||||
ID: v.ID,
|
ID: v.ID,
|
||||||
Token: &v.Token,
|
Token: &token,
|
||||||
Identifier: &v.Identifier,
|
Identifier: &identifier,
|
||||||
Expires: &v.ExpiresAt,
|
Expires: &expires,
|
||||||
CreatedAt: &v.CreatedAt,
|
CreatedAt: &createdAt,
|
||||||
UpdatedAt: &v.UpdatedAt,
|
UpdatedAt: &updatedAt,
|
||||||
Email: &v.Email,
|
Email: &email,
|
||||||
Nonce: &v.Nonce,
|
Nonce: &nonce,
|
||||||
RedirectURI: &v.RedirectURI,
|
RedirectURI: &redirectURI,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
113
server/email/invite_email.go
Normal file
113
server/email/invite_email.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package email
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InviteEmail to send invite email
|
||||||
|
func InviteEmail(toEmail, token, verificationURL, redirectURI string) error {
|
||||||
|
// The receiver needs to be in slice as the receive supports multiple receiver
|
||||||
|
Receiver := []string{toEmail}
|
||||||
|
|
||||||
|
Subject := "Please accept the invitation"
|
||||||
|
message := `
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
|
<meta name="x-apple-disable-message-reformatting">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta content="telephone=no" name="format-detection">
|
||||||
|
<title></title>
|
||||||
|
<!--[if (mso 16)]>
|
||||||
|
<style type="text/css">
|
||||||
|
a {}
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if gte mso 9]><style>sup { font-size: 100%% !important; }</style><![endif]-->
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG></o:AllowPNG>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
<body style="font-family: sans-serif;">
|
||||||
|
<div class="es-wrapper-color">
|
||||||
|
<!--[if gte mso 9]>
|
||||||
|
<v:background xmlns:v="urn:schemas-microsoft-com:vml" fill="t">
|
||||||
|
<v:fill type="tile" color="#ffffff"></v:fill>
|
||||||
|
</v:background>
|
||||||
|
<![endif]-->
|
||||||
|
<table class="es-wrapper" width="100%%" cellspacing="0" cellpadding="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="esd-email-paddings" valign="top">
|
||||||
|
<table class="es-content esd-footer-popover" cellspacing="0" cellpadding="0" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="esd-stripe" align="center">
|
||||||
|
<table class="es-content-body" style="border-left:1px solid transparent;border-right:1px solid transparent;border-top:1px solid transparent;border-bottom:1px solid transparent;padding:20px 0px;" width="600" cellspacing="0" cellpadding="0" bgcolor="#ffffff" align="center">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="esd-structure es-p20t es-p40b es-p40r es-p40l" esd-custom-block-id="8537" align="left">
|
||||||
|
<table width="100%%" cellspacing="0" cellpadding="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="esd-container-frame" width="518" align="left">
|
||||||
|
<table width="100%%" cellspacing="0" cellpadding="0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="esd-block-image es-m-txt-c es-p5b" style="font-size:0;padding:10px" align="center"><a target="_blank" clicktracking="off"><img src="{{.org_logo}}" alt="icon" style="display: block;" title="icon" width="30"></a></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr style="background: rgb(249,250,251);padding: 10px;margin-bottom:10px;border-radius:5px;">
|
||||||
|
<td class="esd-block-text es-m-txt-c es-p15t" align="center" style="padding:10px;padding-bottom:30px;">
|
||||||
|
<p>Hi there 👋</p>
|
||||||
|
<p>Join us! You are invited to sign-up for <b>{{.org_name}}</b>. Please accept the invitation by clicking the clicking the button below.</p> <br/>
|
||||||
|
<a
|
||||||
|
clicktracking="off" href="{{.verification_url}}" class="es-button" target="_blank" style="text-decoration: none;padding:10px 15px;background-color: rgba(59,130,246,1);color: #fff;font-size: 1em;border-radius:5px;">Get Started</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div style="position: absolute; left: -9999px; top: -9999px; margin: 0px;"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
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"] = 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)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
1
server/env/env.go
vendored
1
server/env/env.go
vendored
|
@ -285,6 +285,7 @@ func InitAllEnv() error {
|
||||||
envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true"
|
envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true"
|
||||||
envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true"
|
envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true"
|
||||||
envData.BoolEnv[constants.EnvKeyDisableLoginPage] = os.Getenv(constants.EnvKeyDisableLoginPage) == "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
|
// 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] == "" {
|
if envData.StringEnv[constants.EnvKeySmtpHost] == "" || envData.StringEnv[constants.EnvKeySmtpUsername] == "" || envData.StringEnv[constants.EnvKeySmtpPassword] == "" || envData.StringEnv[constants.EnvKeySenderEmail] == "" && envData.StringEnv[constants.EnvKeySmtpPort] == "" {
|
||||||
|
|
|
@ -41,6 +41,7 @@ var defaultStore = &EnvStore{
|
||||||
constants.EnvKeyDisableMagicLinkLogin: false,
|
constants.EnvKeyDisableMagicLinkLogin: false,
|
||||||
constants.EnvKeyDisableEmailVerification: false,
|
constants.EnvKeyDisableEmailVerification: false,
|
||||||
constants.EnvKeyDisableLoginPage: false,
|
constants.EnvKeyDisableLoginPage: false,
|
||||||
|
constants.EnvKeyDisableSignUp: false,
|
||||||
},
|
},
|
||||||
SliceEnv: map[string][]string{},
|
SliceEnv: map[string][]string{},
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -50,6 +50,7 @@ type Env struct {
|
||||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||||
|
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||||
Roles []string `json:"ROLES"`
|
Roles []string `json:"ROLES"`
|
||||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||||
|
@ -75,6 +76,21 @@ type ForgotPasswordInput struct {
|
||||||
RedirectURI *string `json:"redirect_uri"`
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
type LoginInput struct {
|
type LoginInput struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -99,6 +115,7 @@ type Meta struct {
|
||||||
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
|
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
|
||||||
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
|
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
|
||||||
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
||||||
|
IsSignUpEnabled bool `json:"is_sign_up_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthRevokeInput struct {
|
type OAuthRevokeInput struct {
|
||||||
|
@ -155,6 +172,11 @@ type SignUpInput struct {
|
||||||
ConfirmPassword string `json:"confirm_password"`
|
ConfirmPassword string `json:"confirm_password"`
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
Scope []string `json:"scope"`
|
Scope []string `json:"scope"`
|
||||||
|
RedirectURI *string `json:"redirect_uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateAccessInput struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateEnvInput struct {
|
type UpdateEnvInput struct {
|
||||||
|
@ -180,6 +202,7 @@ type UpdateEnvInput struct {
|
||||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||||
|
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||||
Roles []string `json:"ROLES"`
|
Roles []string `json:"ROLES"`
|
||||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||||
|
@ -242,6 +265,7 @@ type User struct {
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
CreatedAt *int64 `json:"created_at"`
|
CreatedAt *int64 `json:"created_at"`
|
||||||
UpdatedAt *int64 `json:"updated_at"`
|
UpdatedAt *int64 `json:"updated_at"`
|
||||||
|
RevokedTimestamp *int64 `json:"revoked_timestamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Users struct {
|
type Users struct {
|
||||||
|
@ -249,6 +273,16 @@ type Users struct {
|
||||||
Users []*User `json:"users"`
|
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 {
|
type VerificationRequest struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Identifier *string `json:"identifier"`
|
Identifier *string `json:"identifier"`
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Meta {
|
||||||
is_email_verification_enabled: Boolean!
|
is_email_verification_enabled: Boolean!
|
||||||
is_basic_authentication_enabled: Boolean!
|
is_basic_authentication_enabled: Boolean!
|
||||||
is_magic_link_login_enabled: Boolean!
|
is_magic_link_login_enabled: Boolean!
|
||||||
|
is_sign_up_enabled: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type User {
|
type User {
|
||||||
|
@ -42,6 +43,7 @@ type User {
|
||||||
roles: [String!]!
|
roles: [String!]!
|
||||||
created_at: Int64
|
created_at: Int64
|
||||||
updated_at: Int64
|
updated_at: Int64
|
||||||
|
revoked_timestamp: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type Users {
|
type Users {
|
||||||
|
@ -111,6 +113,7 @@ type Env {
|
||||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||||
DISABLE_LOGIN_PAGE: Boolean
|
DISABLE_LOGIN_PAGE: Boolean
|
||||||
|
DISABLE_SIGN_UP: Boolean
|
||||||
ROLES: [String!]
|
ROLES: [String!]
|
||||||
PROTECTED_ROLES: [String!]
|
PROTECTED_ROLES: [String!]
|
||||||
DEFAULT_ROLES: [String!]
|
DEFAULT_ROLES: [String!]
|
||||||
|
@ -125,6 +128,16 @@ type Env {
|
||||||
ORGANIZATION_LOGO: String
|
ORGANIZATION_LOGO: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValidateJWTTokenResponse {
|
||||||
|
is_valid: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenerateJWTKeysResponse {
|
||||||
|
secret: String
|
||||||
|
public_key: String
|
||||||
|
private_key: String
|
||||||
|
}
|
||||||
|
|
||||||
input UpdateEnvInput {
|
input UpdateEnvInput {
|
||||||
ACCESS_TOKEN_EXPIRY_TIME: String
|
ACCESS_TOKEN_EXPIRY_TIME: String
|
||||||
ADMIN_SECRET: String
|
ADMIN_SECRET: String
|
||||||
|
@ -148,6 +161,7 @@ input UpdateEnvInput {
|
||||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||||
DISABLE_LOGIN_PAGE: Boolean
|
DISABLE_LOGIN_PAGE: Boolean
|
||||||
|
DISABLE_SIGN_UP: Boolean
|
||||||
ROLES: [String!]
|
ROLES: [String!]
|
||||||
PROTECTED_ROLES: [String!]
|
PROTECTED_ROLES: [String!]
|
||||||
DEFAULT_ROLES: [String!]
|
DEFAULT_ROLES: [String!]
|
||||||
|
@ -184,6 +198,7 @@ input SignUpInput {
|
||||||
confirm_password: String!
|
confirm_password: String!
|
||||||
roles: [String!]
|
roles: [String!]
|
||||||
scope: [String!]
|
scope: [String!]
|
||||||
|
redirect_uri: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input LoginInput {
|
input LoginInput {
|
||||||
|
@ -274,6 +289,25 @@ input OAuthRevokeInput {
|
||||||
refresh_token: String!
|
refresh_token: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input InviteMemberInput {
|
||||||
|
emails: [String!]!
|
||||||
|
redirect_uri: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateAccessInput {
|
||||||
|
user_id: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
input ValidateJWTTokenInput {
|
||||||
|
token_type: String!
|
||||||
|
token: String!
|
||||||
|
roles: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
|
input GenerateJWTKeysInput {
|
||||||
|
type: String!
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
signup(params: SignUpInput!): AuthResponse!
|
signup(params: SignUpInput!): AuthResponse!
|
||||||
login(params: LoginInput!): AuthResponse!
|
login(params: LoginInput!): AuthResponse!
|
||||||
|
@ -292,12 +326,17 @@ type Mutation {
|
||||||
_admin_login(params: AdminLoginInput!): Response!
|
_admin_login(params: AdminLoginInput!): Response!
|
||||||
_admin_logout: Response!
|
_admin_logout: Response!
|
||||||
_update_env(params: UpdateEnvInput!): 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 {
|
type Query {
|
||||||
meta: Meta!
|
meta: Meta!
|
||||||
session(params: SessionQueryInput): AuthResponse!
|
session(params: SessionQueryInput): AuthResponse!
|
||||||
profile: User!
|
profile: User!
|
||||||
|
validate_jwt_token(params: ValidateJWTTokenInput!): ValidateJWTTokenResponse!
|
||||||
# admin only apis
|
# admin only apis
|
||||||
_users(params: PaginatedInput): Users!
|
_users(params: PaginatedInput): Users!
|
||||||
_verification_requests(params: PaginatedInput): VerificationRequests!
|
_verification_requests(params: PaginatedInput): VerificationRequests!
|
||||||
|
|
|
@ -75,6 +75,22 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
|
||||||
return resolvers.UpdateEnvResolver(ctx, params)
|
return resolvers.UpdateEnvResolver(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
|
||||||
|
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) {
|
func (r *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
|
||||||
return resolvers.MetaResolver(ctx)
|
return resolvers.MetaResolver(ctx)
|
||||||
}
|
}
|
||||||
|
@ -87,6 +103,10 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
|
||||||
return resolvers.ProfileResolver(ctx)
|
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) {
|
func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
|
||||||
return resolvers.UsersResolver(ctx, params)
|
return resolvers.UsersResolver(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -51,6 +52,8 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||||
gc.JSON(400, gin.H{"error": "invalid response mode"})
|
gc.JSON(400, gin.H{"error": "invalid response mode"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("=> redirect URI:", redirectURI)
|
||||||
|
fmt.Println("=> state:", state)
|
||||||
if redirectURI == "" {
|
if redirectURI == "" {
|
||||||
redirectURI = "/app"
|
redirectURI = "/app"
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,10 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||||
existingUser, err := db.Provider.GetUserByEmail(user.Email)
|
existingUser, err := db.Provider.GetUserByEmail(user.Email)
|
||||||
|
|
||||||
if err != nil {
|
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 not registered, register user and generate session token
|
||||||
user.SignupMethods = provider
|
user.SignupMethods = provider
|
||||||
// make sure inputRoles don't include protected roles
|
// make sure inputRoles don't include protected roles
|
||||||
|
@ -91,9 +95,12 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||||
user.EmailVerifiedAt = &now
|
user.EmailVerifiedAt = &now
|
||||||
user, _ = db.Provider.AddUser(user)
|
user, _ = db.Provider.AddUser(user)
|
||||||
} else {
|
} 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
|
// user exists in db, check if method was google
|
||||||
// if not append google to existing signup method and save it
|
// if not append google to existing signup method and save it
|
||||||
|
|
||||||
signupMethod := existingUser.SignupMethods
|
signupMethod := existingUser.SignupMethods
|
||||||
if !strings.Contains(signupMethod, provider) {
|
if !strings.Contains(signupMethod, provider) {
|
||||||
signupMethod = signupMethod + "," + provider
|
signupMethod = signupMethod + "," + provider
|
||||||
|
|
|
@ -16,7 +16,11 @@ import (
|
||||||
func OAuthLoginHandler() gin.HandlerFunc {
|
func OAuthLoginHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
hostname := utils.GetHost(c)
|
hostname := utils.GetHost(c)
|
||||||
|
// deprecating redirectURL instead use redirect_uri
|
||||||
redirectURI := strings.TrimSpace(c.Query("redirectURL"))
|
redirectURI := strings.TrimSpace(c.Query("redirectURL"))
|
||||||
|
if redirectURI == "" {
|
||||||
|
redirectURI = strings.TrimSpace(c.Query("redirect_uri"))
|
||||||
|
}
|
||||||
roles := strings.TrimSpace(c.Query("roles"))
|
roles := strings.TrimSpace(c.Query("roles"))
|
||||||
state := strings.TrimSpace(c.Query("state"))
|
state := strings.TrimSpace(c.Query("state"))
|
||||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||||
|
|
|
@ -111,8 +111,6 @@ func TokenHandler() gin.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// rollover the session for security
|
|
||||||
sessionstore.RemoveState(sessionDataSplit[1])
|
|
||||||
// validate session
|
// validate session
|
||||||
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,6 +120,8 @@ func TokenHandler() gin.HandlerFunc {
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// rollover the session for security
|
||||||
|
sessionstore.RemoveState(sessionDataSplit[1])
|
||||||
userID = claims.Subject
|
userID = claims.Subject
|
||||||
roles = claims.Roles
|
roles = claims.Roles
|
||||||
scope = claims.Scope
|
scope = claims.Scope
|
||||||
|
|
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
|
||||||
|
}
|
|
@ -54,6 +54,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||||
disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]
|
disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]
|
||||||
disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]
|
disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]
|
||||||
disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage]
|
disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage]
|
||||||
|
disableSignUp := store.BoolEnv[constants.EnvKeyDisableSignUp]
|
||||||
roles := store.SliceEnv[constants.EnvKeyRoles]
|
roles := store.SliceEnv[constants.EnvKeyRoles]
|
||||||
defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles]
|
defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles]
|
||||||
protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles]
|
protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles]
|
||||||
|
@ -94,6 +95,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||||
DisableBasicAuthentication: &disableBasicAuthentication,
|
DisableBasicAuthentication: &disableBasicAuthentication,
|
||||||
DisableMagicLinkLogin: &disableMagicLinkLogin,
|
DisableMagicLinkLogin: &disableMagicLinkLogin,
|
||||||
DisableLoginPage: &disableLoginPage,
|
DisableLoginPage: &disableLoginPage,
|
||||||
|
DisableSignUp: &disableSignUp,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
ProtectedRoles: protectedRoles,
|
ProtectedRoles: protectedRoles,
|
||||||
DefaultRoles: defaultRoles,
|
DefaultRoles: defaultRoles,
|
||||||
|
|
|
@ -43,7 +43,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
redirectURL := utils.GetAppURL(gc) + "/reset-password"
|
||||||
if params.RedirectURI != nil {
|
if params.RedirectURI != nil {
|
||||||
redirectURL = *params.RedirectURI
|
redirectURL = *params.RedirectURI
|
||||||
}
|
}
|
||||||
|
|
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")
|
||||||
|
}
|
135
server/resolvers/invite_members.go
Normal file
135
server/resolvers/invite_members.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package resolvers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
emailservice "github.com/authorizerdev/authorizer/server/email"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InviteMembersResolver resolver to invite members
|
||||||
|
func InviteMembersResolver(ctx context.Context, params model.InviteMemberInput) (*model.Response, error) {
|
||||||
|
gc, err := utils.GinContextFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !token.IsSuperAdmin(gc) {
|
||||||
|
return nil, errors.New("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// this feature is only allowed if email server is configured
|
||||||
|
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||||
|
return nil, errors.New("email sending is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) && envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
||||||
|
return nil, errors.New("either basic authentication or magic link login is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter valid emails
|
||||||
|
emails := []string{}
|
||||||
|
for _, email := range params.Emails {
|
||||||
|
if utils.IsValidEmail(email) {
|
||||||
|
emails = append(emails, email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(emails) == 0 {
|
||||||
|
return nil, errors.New("no valid emails found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: optimise to use like query instead of looping through emails and getting user individually
|
||||||
|
// for each emails check if emails exists in db
|
||||||
|
newEmails := []string{}
|
||||||
|
for _, email := range emails {
|
||||||
|
_, err := db.Provider.GetUserByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s user not found. inviting user.", email)
|
||||||
|
newEmails = append(newEmails, email)
|
||||||
|
} else {
|
||||||
|
log.Println("%s user already exists. skipping.", email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newEmails) == 0 {
|
||||||
|
return nil, errors.New("all emails already exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
// invite new emails
|
||||||
|
for _, email := range newEmails {
|
||||||
|
|
||||||
|
user := models.User{
|
||||||
|
Email: email,
|
||||||
|
Roles: strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ","),
|
||||||
|
}
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
|
verifyEmailURL := hostname + "/verify_email"
|
||||||
|
appURL := utils.GetAppURL(gc)
|
||||||
|
|
||||||
|
redirectURL := appURL
|
||||||
|
if params.RedirectURI != nil {
|
||||||
|
redirectURL = *params.RedirectURI
|
||||||
|
}
|
||||||
|
|
||||||
|
_, nonceHash, err := utils.GenerateNonce()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationToken, err := token.CreateVerificationToken(email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(`error generating token`, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verificationRequest := models.VerificationRequest{
|
||||||
|
Token: verificationToken,
|
||||||
|
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||||
|
Email: email,
|
||||||
|
Nonce: nonceHash,
|
||||||
|
RedirectURI: redirectURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// use magic link login if that option is on
|
||||||
|
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
||||||
|
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||||
|
verificationRequest.Identifier = constants.VerificationTypeMagicLinkLogin
|
||||||
|
} else {
|
||||||
|
// use basic authentication if that option is on
|
||||||
|
user.SignupMethods = constants.SignupMethodBasicAuth
|
||||||
|
verificationRequest.Identifier = constants.VerificationTypeForgotPassword
|
||||||
|
|
||||||
|
verifyEmailURL = appURL + "/setup-password"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err = db.Provider.AddUser(user)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error inviting user: %s, err: %v", email, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Provider.AddVerificationRequest(verificationRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error inviting user: %s, err: %v", email, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
go emailservice.InviteEmail(email, verificationToken, verifyEmailURL, redirectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.Response{
|
||||||
|
Message: fmt.Sprintf("%d user(s) invited successfully.", len(newEmails)),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -36,6 +36,10 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||||
return res, fmt.Errorf(`user with this email not found`)
|
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) {
|
if !strings.Contains(user.SignupMethods, constants.SignupMethodBasicAuth) {
|
||||||
return res, fmt.Errorf(`user has not signed up email & password`)
|
return res, fmt.Errorf(`user has not signed up email & password`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,8 +43,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||||
|
|
||||||
// find user with email
|
// find user with email
|
||||||
existingUser, err := db.Provider.GetUserByEmail(params.Email)
|
existingUser, err := db.Provider.GetUserByEmail(params.Email)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||||
|
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||||
|
}
|
||||||
|
|
||||||
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||||
// define roles for new user
|
// define roles for new user
|
||||||
if len(params.Roles) > 0 {
|
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.
|
// 2. user has not signed up for one of the available role but trying to signup.
|
||||||
// Need to modify roles in this case
|
// Need to modify roles in this case
|
||||||
|
|
||||||
|
if user.RevokedTimestamp != nil {
|
||||||
|
return res, fmt.Errorf(`user access has been revoked`)
|
||||||
|
}
|
||||||
|
|
||||||
// find the unassigned roles
|
// find the unassigned roles
|
||||||
if len(params.Roles) <= 0 {
|
if len(params.Roles) <= 0 {
|
||||||
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||||
|
@ -123,7 +130,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||||
if params.Scope != nil && len(params.Scope) > 0 {
|
if params.Scope != nil && len(params.Scope) > 0 {
|
||||||
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
|
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
|
||||||
}
|
}
|
||||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
redirectURL := utils.GetAppURL(gc)
|
||||||
if params.RedirectURI != nil {
|
if params.RedirectURI != nil {
|
||||||
redirectURL = *params.RedirectURI
|
redirectURL = *params.RedirectURI
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,10 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
|
||||||
return res, fmt.Errorf(`passwords don't match`)
|
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
|
// verify if token exists in db
|
||||||
hostname := utils.GetHost(gc)
|
hostname := utils.GetHost(gc)
|
||||||
claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
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
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ package resolvers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/cookie"
|
"github.com/authorizerdev/authorizer/server/cookie"
|
||||||
|
@ -25,13 +27,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
|
||||||
|
|
||||||
sessionToken, err := cookie.GetSession(gc)
|
sessionToken, err := cookie.GetSession(gc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
log.Println("error getting session token:", err)
|
||||||
|
return res, errors.New("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// get session from cookie
|
// get session from cookie
|
||||||
claims, err := token.ValidateBrowserSession(gc, sessionToken)
|
claims, err := token.ValidateBrowserSession(gc, sessionToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
log.Println("session validation failed:", err)
|
||||||
|
return res, errors.New("unauthorized")
|
||||||
}
|
}
|
||||||
userID := claims.Subject
|
userID := claims.Subject
|
||||||
user, err := db.Provider.GetUserByID(userID)
|
user, err := db.Provider.GetUserByID(userID)
|
||||||
|
|
|
@ -28,13 +28,22 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||||
return res, err
|
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) {
|
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
||||||
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.ConfirmPassword != params.Password {
|
if params.ConfirmPassword != params.Password {
|
||||||
return res, fmt.Errorf(`password and confirm password does not match`)
|
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)
|
params.Email = strings.ToLower(params.Email)
|
||||||
|
|
||||||
if !utils.IsValidEmail(params.Email) {
|
if !utils.IsValidEmail(params.Email) {
|
||||||
|
@ -128,7 +137,10 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
verificationType := constants.VerificationTypeBasicAuthSignup
|
verificationType := constants.VerificationTypeBasicAuthSignup
|
||||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
redirectURL := utils.GetAppURL(gc)
|
||||||
|
if params.RedirectURI != nil {
|
||||||
|
redirectURL = *params.RedirectURI
|
||||||
|
}
|
||||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
|
|
|
@ -53,11 +53,19 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
||||||
}
|
}
|
||||||
|
|
||||||
if isJWTUpdated {
|
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
|
// check if jwt secret is provided
|
||||||
if crypto.IsHMACA(algo) {
|
if crypto.IsHMACA(algo) {
|
||||||
if params.JwtSecret == nil {
|
if params.JwtSecret == nil {
|
||||||
return res, fmt.Errorf("jwt secret is required for HMAC algorithm")
|
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) {
|
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")
|
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)
|
_, err = crypto.ParseRsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
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")
|
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)
|
_, err = crypto.ParseEcdsaPrivateKeyFromPemStr(*params.JwtPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
|
|
|
@ -134,7 +134,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
verificationType := constants.VerificationTypeUpdateEmail
|
verificationType := constants.VerificationTypeUpdateEmail
|
||||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
redirectURL := utils.GetAppURL(gc)
|
||||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
|
|
|
@ -106,7 +106,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
verificationType := constants.VerificationTypeUpdateEmail
|
verificationType := constants.VerificationTypeUpdateEmail
|
||||||
redirectURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
redirectURL := utils.GetAppURL(gc)
|
||||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
|
@ -154,6 +154,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createdAt := user.CreatedAt
|
||||||
|
updatedAt := user.UpdatedAt
|
||||||
res = &model.User{
|
res = &model.User{
|
||||||
ID: params.ID,
|
ID: params.ID,
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
|
@ -161,8 +163,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||||
GivenName: user.GivenName,
|
GivenName: user.GivenName,
|
||||||
FamilyName: user.FamilyName,
|
FamilyName: user.FamilyName,
|
||||||
Roles: strings.Split(user.Roles, ","),
|
Roles: strings.Split(user.Roles, ","),
|
||||||
CreatedAt: &user.CreatedAt,
|
CreatedAt: &createdAt,
|
||||||
UpdatedAt: &user.UpdatedAt,
|
UpdatedAt: &updatedAt,
|
||||||
}
|
}
|
||||||
return res, nil
|
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
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ func InitRouter() *gin.Engine {
|
||||||
{
|
{
|
||||||
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
||||||
dashboard.Static("/build", "dashboard/build")
|
dashboard.Static("/build", "dashboard/build")
|
||||||
|
dashboard.Static("/public", "dashboard/public")
|
||||||
dashboard.GET("/", handlers.DashboardHandler())
|
dashboard.GET("/", handlers.DashboardHandler())
|
||||||
dashboard.GET("/:page", 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 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)
|
||||||
|
|
||||||
|
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 revoked 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
58
server/test/invite_member_test.go
Normal file
58
server/test/invite_member_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
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 inviteUserTest(t *testing.T, s TestSetup) {
|
||||||
|
t.Helper()
|
||||||
|
t.Run(`should invite user successfully`, func(t *testing.T) {
|
||||||
|
req, ctx := createContext(s)
|
||||||
|
emails := []string{"invite_member1." + s.TestInfo.Email}
|
||||||
|
|
||||||
|
// unauthorized error
|
||||||
|
res, err := resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||||
|
Emails: emails,
|
||||||
|
})
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
// invalid emails test
|
||||||
|
invalidEmailsTest := []string{
|
||||||
|
"test",
|
||||||
|
"test.com",
|
||||||
|
}
|
||||||
|
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||||
|
Emails: invalidEmailsTest,
|
||||||
|
})
|
||||||
|
|
||||||
|
// valid test
|
||||||
|
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||||
|
Emails: emails,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, res)
|
||||||
|
|
||||||
|
// duplicate error test
|
||||||
|
res, err = resolvers.InviteMembersResolver(ctx, model.InviteMemberInput{
|
||||||
|
Emails: emails,
|
||||||
|
})
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, res)
|
||||||
|
|
||||||
|
cleanData(emails[0])
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/db"
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
t.Run(`should login with magic link`, func(t *testing.T) {
|
||||||
req, ctx := createContext(s)
|
req, ctx := createContext(s)
|
||||||
email := "magic_link_login." + s.TestInfo.Email
|
email := "magic_link_login." + s.TestInfo.Email
|
||||||
|
envstore.EnvStoreObj.UpdateEnvVariable(constants.BoolStoreIdentifier, constants.EnvKeyDisableSignUp, true)
|
||||||
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
_, err := resolvers.MagicLinkLoginResolver(ctx, model.MagicLinkLoginInput{
|
||||||
Email: email,
|
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)
|
verificationRequest, err := db.Provider.GetVerificationRequestByEmail(email, constants.VerificationTypeMagicLinkLogin)
|
||||||
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
verifyRes, err := resolvers.VerifyEmailResolver(ctx, model.VerifyEmailInput{
|
||||||
|
|
|
@ -43,6 +43,14 @@ func resetPasswordTest(t *testing.T, s TestSetup) {
|
||||||
ConfirmPassword: "test1",
|
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")
|
assert.Nil(t, err, "password changed successfully")
|
||||||
|
|
||||||
cleanData(email)
|
cleanData(email)
|
||||||
|
|
|
@ -48,6 +48,9 @@ func TestResolvers(t *testing.T) {
|
||||||
adminSessionTests(t, s)
|
adminSessionTests(t, s)
|
||||||
updateEnvTests(t, s)
|
updateEnvTests(t, s)
|
||||||
envTests(t, s)
|
envTests(t, s)
|
||||||
|
revokeAccessTest(t, s)
|
||||||
|
enableAccessTest(t, s)
|
||||||
|
generateJWTkeyTest(t, s)
|
||||||
|
|
||||||
// user tests
|
// user tests
|
||||||
loginTests(t, s)
|
loginTests(t, s)
|
||||||
|
@ -62,6 +65,8 @@ func TestResolvers(t *testing.T) {
|
||||||
magicLinkLoginTests(t, s)
|
magicLinkLoginTests(t, s)
|
||||||
logoutTests(t, s)
|
logoutTests(t, s)
|
||||||
metaTests(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/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/db"
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -20,14 +21,30 @@ func signupTests(t *testing.T, s TestSetup) {
|
||||||
Password: s.TestInfo.Password,
|
Password: s.TestInfo.Password,
|
||||||
ConfirmPassword: s.TestInfo.Password + "s",
|
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{
|
res, err = resolvers.SignupResolver(ctx, model.SignUpInput{
|
||||||
Email: email,
|
Email: email,
|
||||||
Password: s.TestInfo.Password,
|
Password: s.TestInfo.Password,
|
||||||
ConfirmPassword: 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
|
user := *res.User
|
||||||
assert.Equal(t, email, user.Email)
|
assert.Equal(t, email, user.Email)
|
||||||
assert.Nil(t, res.AccessToken, "access token should be nil")
|
assert.Nil(t, res.AccessToken, "access token should be nil")
|
||||||
|
|
|
@ -68,7 +68,7 @@ func createContext(s TestSetup) (*http.Request, context.Context) {
|
||||||
func testSetup() TestSetup {
|
func testSetup() TestSetup {
|
||||||
testData := TestData{
|
testData := TestData{
|
||||||
Email: fmt.Sprintf("%d_authorizer_tester@yopmail.com", time.Now().Unix()),
|
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")
|
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.VerificationTypeUpdateEmail), "it should be valid identifier")
|
||||||
assert.True(t, utils.IsValidVerificationIdentifier(constants.VerificationTypeForgotPassword), "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")
|
||||||
|
}
|
||||||
|
|
|
@ -165,7 +165,12 @@ func GetAccessToken(gc *gin.Context) (string, error) {
|
||||||
return "", fmt.Errorf(`unauthorized`)
|
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`)
|
return "", fmt.Errorf(`not a bearer token`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,7 +363,12 @@ func GetIDToken(gc *gin.Context) (string, error) {
|
||||||
return "", fmt.Errorf(`unauthorized`)
|
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`)
|
return "", fmt.Errorf(`not a bearer token`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,3 +105,59 @@ func ParseJWTToken(token, hostname, nonce, subject string) (jwt.MapClaims, error
|
||||||
|
|
||||||
return claims, nil
|
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 (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/db"
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
@ -47,3 +48,24 @@ func RemoveDuplicateString(strSlice []string) []string {
|
||||||
}
|
}
|
||||||
return list
|
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),
|
IsBasicAuthenticationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication),
|
||||||
IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification),
|
IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification),
|
||||||
IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin),
|
IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin),
|
||||||
|
IsSignUpEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
|
||||||
|
|
||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAppURL to get /app/ url if not configured by user
|
||||||
|
func GetAppURL(gc *gin.Context) string {
|
||||||
|
envAppURL := envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)
|
||||||
|
if envAppURL == "" {
|
||||||
|
envAppURL = GetHost(gc) + "/app"
|
||||||
|
}
|
||||||
|
return envAppURL
|
||||||
|
}
|
||||||
|
|
|
@ -86,3 +86,35 @@ func IsStringArrayEqual(a, b []string) bool {
|
||||||
}
|
}
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user