Compare commits
51 Commits
0.14.0-bet
...
0.15.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3aa888b14e | ||
![]() |
30be32a10b | ||
![]() |
69d781d6cf | ||
![]() |
e4d9c60971 | ||
![]() |
96edb43b67 | ||
![]() |
21fef67c7d | ||
![]() |
9f09823c8b | ||
![]() |
1a64149da7 | ||
![]() |
99b846811a | ||
![]() |
df7837f44d | ||
![]() |
d709f53c47 | ||
![]() |
a257b77501 | ||
![]() |
2213619ed5 | ||
![]() |
f65ea72944 | ||
![]() |
32f8c99a71 | ||
![]() |
8ec52a90f1 | ||
![]() |
2498958295 | ||
![]() |
2913fa0603 | ||
![]() |
e126bfddad | ||
![]() |
83001b859c | ||
![]() |
74a8024131 | ||
![]() |
5e6ee8d9b0 | ||
![]() |
3e7150f872 | ||
![]() |
9a19552f72 | ||
![]() |
ab01ff249d | ||
![]() |
1b387f7564 | ||
![]() |
8e79ab77b2 | ||
![]() |
2bf6b8f91d | ||
![]() |
776c0fba8b | ||
![]() |
dd64aa2e79 | ||
![]() |
157b13baa7 | ||
![]() |
d1e284116d | ||
![]() |
2f9725d8e1 | ||
![]() |
ee7aea7bee | ||
![]() |
5d73df0040 | ||
![]() |
60cd317e67 | ||
![]() |
f5bdc8db39 | ||
![]() |
9eca697a91 | ||
![]() |
7136ee924d | ||
![]() |
fd9eb7c733 | ||
![]() |
917eaeb2ed | ||
![]() |
3bb90acc9e | ||
![]() |
a69b8e290c | ||
![]() |
674eeeea4e | ||
![]() |
8c2bf6ee0d | ||
![]() |
57bc091499 | ||
![]() |
128a2a8f75 | ||
![]() |
7b09a8817c | ||
![]() |
1d61840c6d | ||
![]() |
4b25e8941c | ||
![]() |
136eda15bf |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,4 +11,6 @@ data.db
|
||||
.DS_Store
|
||||
.env.local
|
||||
*.tar.gz
|
||||
.vscode/
|
||||
.vscode/
|
||||
.yalc
|
||||
yalc.lock
|
30
app/package-lock.json
generated
30
app/package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-react": "0.7.0",
|
||||
"@authorizerdev/authorizer-react": "latest",
|
||||
"@types/react": "^17.0.15",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"esbuild": "^0.12.17",
|
||||
@@ -24,9 +24,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz",
|
||||
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz",
|
||||
"integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1"
|
||||
},
|
||||
@@ -35,11 +35,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@authorizerdev/authorizer-react": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz",
|
||||
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz",
|
||||
"integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==",
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": "^0.3.0",
|
||||
"@authorizerdev/authorizer-js": "^0.6.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
@@ -829,19 +829,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@authorizerdev/authorizer-js": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.3.0.tgz",
|
||||
"integrity": "sha512-KCE5Dw5MUnEgstBUayBriDQAOjqbxU7ixC00rTHAE6aD6TxJkeSls0vCTXpvt4iiKhFK6q9BhHwa/5NwWYpDBQ==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-js/-/authorizer-js-0.6.0.tgz",
|
||||
"integrity": "sha512-WbqeUmhQwLNlvk4ZYTptlbAIINh7aZPyTCVA/B0FE3EoPtx1tNOtkPtJOycrn0H0HyueeXQnBSCDxkvPAP65Bw==",
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"@authorizerdev/authorizer-react": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.7.0.tgz",
|
||||
"integrity": "sha512-cAxUhodftIveSQt+rFuEA0CxjmbpVfE43ioZBwBxqWEuJHPdPH7bohOQRgTyA2xb3QVnh7kr607Tau13DO7qUA==",
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@authorizerdev/authorizer-react/-/authorizer-react-0.11.0.tgz",
|
||||
"integrity": "sha512-VzSZvEB/t6N2ESn4O8c/+2hPUO7L4Iux8IBzXKrobKkoqRyb+u5TPZn0UWCOaoxIdiiZY+1Yq2A/H6q9LAqLGw==",
|
||||
"requires": {
|
||||
"@authorizerdev/authorizer-js": "^0.3.0",
|
||||
"@authorizerdev/authorizer-js": "^0.6.0",
|
||||
"final-form": "^4.20.2",
|
||||
"react-final-form": "^6.5.3",
|
||||
"styled-components": "^5.3.0"
|
||||
|
@@ -2,10 +2,33 @@ import React from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
|
||||
import Root from './Root';
|
||||
import { createRandomString } from './utils/common';
|
||||
|
||||
export default function App() {
|
||||
// @ts-ignore
|
||||
const globalState: Record<string, string> = window['__authorizer__'];
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const state = searchParams.get('state') || createRandomString();
|
||||
const scope = searchParams.get('scope')
|
||||
? searchParams.get('scope')?.toString().split(' ')
|
||||
: `openid profile email`;
|
||||
|
||||
const urlProps: Record<string, any> = {
|
||||
state,
|
||||
scope,
|
||||
};
|
||||
|
||||
const redirectURL =
|
||||
searchParams.get('redirect_uri') || searchParams.get('redirectURL');
|
||||
if (redirectURL) {
|
||||
urlProps.redirectURL = redirectURL;
|
||||
} else {
|
||||
urlProps.redirectURL = window.location.origin + '/app';
|
||||
}
|
||||
const globalState: Record<string, string> = {
|
||||
// @ts-ignore
|
||||
...window['__authorizer__'],
|
||||
...urlProps,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -30,15 +53,7 @@ export default function App() {
|
||||
/>
|
||||
<h1>{globalState.organizationName}</h1>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: 400,
|
||||
margin: `10px auto`,
|
||||
border: `1px solid #D1D5DB`,
|
||||
padding: `25px 20px`,
|
||||
borderRadius: 5,
|
||||
}}
|
||||
>
|
||||
<div className="container">
|
||||
<BrowserRouter>
|
||||
<AuthorizerProvider
|
||||
config={{
|
||||
@@ -46,7 +61,7 @@ export default function App() {
|
||||
redirectURL: globalState.redirectURL,
|
||||
}}
|
||||
>
|
||||
<Root />
|
||||
<Root globalState={globalState} />
|
||||
</AuthorizerProvider>
|
||||
</BrowserRouter>
|
||||
</div>
|
||||
|
@@ -1,19 +1,36 @@
|
||||
import React, { useEffect, lazy, Suspense } from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { useAuthorizer } from '@authorizerdev/authorizer-react';
|
||||
import SetupPassword from './pages/setup-password';
|
||||
|
||||
const ResetPassword = lazy(() => import('./pages/rest-password'));
|
||||
const Login = lazy(() => import('./pages/login'));
|
||||
const Dashboard = lazy(() => import('./pages/dashboard'));
|
||||
|
||||
export default function Root() {
|
||||
export default function Root({
|
||||
globalState,
|
||||
}: {
|
||||
globalState: Record<string, string>;
|
||||
}) {
|
||||
const { token, loading, config } = useAuthorizer();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
const url = new URL(config.redirectURL || '/app');
|
||||
let redirectURL = config.redirectURL || '/app';
|
||||
let params = `access_token=${token.access_token}&id_token=${token.id_token}&expires_in=${token.expires_in}&state=${globalState.state}`;
|
||||
if (token.refresh_token) {
|
||||
params += `&refresh_token=${token.refresh_token}`;
|
||||
}
|
||||
const url = new URL(redirectURL);
|
||||
if (redirectURL.includes('?')) {
|
||||
redirectURL = `${redirectURL}&${params}`;
|
||||
} else {
|
||||
redirectURL = `${redirectURL}?${params}`;
|
||||
}
|
||||
|
||||
if (url.origin !== window.location.origin) {
|
||||
window.location.href = config.redirectURL || '/app';
|
||||
sessionStorage.removeItem('authorizer_state');
|
||||
window.location.replace(redirectURL);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
@@ -44,6 +61,9 @@ export default function Root() {
|
||||
<Route path="/app/reset-password">
|
||||
<ResetPassword />
|
||||
</Route>
|
||||
<Route path="/app/setup-password">
|
||||
<SetupPassword />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Suspense>
|
||||
);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
body {
|
||||
margin: 0;
|
||||
margin: 10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
@@ -14,3 +14,17 @@ body {
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: content-box;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 25px 20px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 768px) {
|
||||
.container {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
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>
|
||||
);
|
||||
}
|
22
app/src/utils/common.ts
Normal file
22
app/src/utils/common.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const getCrypto = () => {
|
||||
//ie 11.x uses msCrypto
|
||||
return (window.crypto || (window as any).msCrypto) as Crypto;
|
||||
};
|
||||
|
||||
export const createRandomString = () => {
|
||||
const charset =
|
||||
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.';
|
||||
let random = '';
|
||||
const randomValues = Array.from(
|
||||
getCrypto().getRandomValues(new Uint8Array(43))
|
||||
);
|
||||
randomValues.forEach((v) => (random += charset[v % charset.length]));
|
||||
return random;
|
||||
};
|
||||
|
||||
export const createQueryParams = (params: any) => {
|
||||
return Object.keys(params)
|
||||
.filter((k) => typeof params[k] !== 'undefined')
|
||||
.map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&');
|
||||
};
|
71
dashboard/package-lock.json
generated
71
dashboard/package-lock.json
generated
@@ -22,6 +22,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"typescript": "^4.5.4",
|
||||
@@ -1251,6 +1252,14 @@
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-macros": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
||||
@@ -1631,6 +1640,17 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/file-selector": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
|
||||
"integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -1914,9 +1934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -1959,6 +1979,22 @@
|
||||
"react": "17.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
|
||||
"integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==",
|
||||
"dependencies": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.4.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
@@ -3226,6 +3262,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"attr-accept": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg=="
|
||||
},
|
||||
"babel-plugin-macros": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz",
|
||||
@@ -3478,6 +3519,14 @@
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"file-selector": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz",
|
||||
"integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==",
|
||||
"requires": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"find-root": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||
@@ -3707,9 +3756,9 @@
|
||||
}
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.0.tgz",
|
||||
"integrity": "sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g==",
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -3743,6 +3792,16 @@
|
||||
"scheduler": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-12.0.4.tgz",
|
||||
"integrity": "sha512-fcqHEYe1MzAghU6/Hz86lHDlBNsA+lO48nAcm7/wA+kIzwS6uuJbUG33tBZjksj7GAZ1iUQ6NHwjUURPmSGang==",
|
||||
"requires": {
|
||||
"attr-accept": "^2.2.2",
|
||||
"file-selector": "^0.4.0",
|
||||
"prop-types": "^15.8.1"
|
||||
}
|
||||
},
|
||||
"react-fast-compare": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
|
||||
|
@@ -24,6 +24,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dropzone": "^12.0.4",
|
||||
"react-icons": "^4.3.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"typescript": "^4.5.4",
|
||||
|
1
dashboard/public/sample.csv
Normal file
1
dashboard/public/sample.csv
Normal file
@@ -0,0 +1 @@
|
||||
foo@bar.com,test@authorizer.dev
|
|
370
dashboard/src/components/InviteMembersModal.tsx
Normal file
370
dashboard/src/components/InviteMembersModal.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
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 { escape } from 'lodash';
|
||||
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;
|
@@ -29,10 +29,11 @@ import {
|
||||
} from 'react-icons/fi';
|
||||
import { IconType } from 'react-icons';
|
||||
import { ReactText } from 'react';
|
||||
import { useMutation } from 'urql';
|
||||
import { useMutation, useQuery } from 'urql';
|
||||
import { NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthContext } from '../contexts/AuthContext';
|
||||
import { AdminLogout } from '../graphql/mutation';
|
||||
import { MetaQuery } from '../graphql/queries';
|
||||
|
||||
interface LinkItemProps {
|
||||
name: string;
|
||||
@@ -51,6 +52,7 @@ interface SidebarProps extends BoxProps {
|
||||
|
||||
export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
||||
const { pathname } = useLocation();
|
||||
const [{ fetching, data }] = useQuery({ query: MetaQuery });
|
||||
return (
|
||||
<Box
|
||||
transition="3s ease"
|
||||
@@ -98,6 +100,19 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
||||
>
|
||||
<NavItem icon={FiCode}>API Playground</NavItem>
|
||||
</Link>
|
||||
|
||||
{data?.meta?.version && (
|
||||
<Text
|
||||
color="gray.600"
|
||||
fontSize="sm"
|
||||
textAlign="center"
|
||||
position="absolute"
|
||||
bottom="5"
|
||||
left="7"
|
||||
>
|
||||
Current Version: {data.meta.version}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@@ -60,6 +60,7 @@ export const SwitchInputType = {
|
||||
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
|
||||
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
|
||||
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
|
||||
DISABLE_SIGN_UP: 'DISABLE_SIGN_UP',
|
||||
};
|
||||
|
||||
export const DateInputType = {
|
||||
|
@@ -45,3 +45,11 @@ export const DeleteUser = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const InviteMembers = `
|
||||
mutation inviteMembers($params: InviteMemberInput!) {
|
||||
_invite_members(params: $params) {
|
||||
message
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -1,3 +1,12 @@
|
||||
export const MetaQuery = `
|
||||
query MetaQuery {
|
||||
meta {
|
||||
version
|
||||
client_id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const AdminSessionQuery = `
|
||||
query {
|
||||
_admin_session{
|
||||
@@ -39,6 +48,7 @@ export const EnvVariablesQuery = `
|
||||
DISABLE_MAGIC_LINK_LOGIN,
|
||||
DISABLE_EMAIL_VERIFICATION,
|
||||
DISABLE_BASIC_AUTHENTICATION,
|
||||
DISABLE_SIGN_UP,
|
||||
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||
DATABASE_NAME,
|
||||
DATABASE_TYPE,
|
||||
@@ -75,3 +85,11 @@ export const UserDetailsQuery = `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EmailVerificationQuery = `
|
||||
query {
|
||||
_env{
|
||||
DISABLE_EMAIL_VERIFICATION
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import { Box, Center, Flex, Image, Text } from '@chakra-ui/react';
|
||||
import { Box, Flex, Image, Text, Spinner } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { LOGO_URL } from '../constants';
|
||||
import { useQuery } from 'urql';
|
||||
import { MetaQuery } from '../graphql/queries';
|
||||
|
||||
export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
const [{ fetching, data }] = useQuery({ query: MetaQuery });
|
||||
return (
|
||||
<Flex
|
||||
flexWrap="wrap"
|
||||
@@ -23,9 +25,18 @@ export function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
|
||||
{children}
|
||||
</Box>
|
||||
{fetching ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<Box p="6" m="5" rounded="5" bg="white" w="500px" shadow="xl">
|
||||
{children}
|
||||
</Box>
|
||||
<Text color="gray.600" fontSize="sm">
|
||||
Current Version: {data.meta.version}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import {
|
||||
useToast,
|
||||
VStack,
|
||||
Text,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useMutation } from 'urql';
|
||||
|
@@ -68,6 +68,7 @@ interface envVarTypes {
|
||||
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;
|
||||
@@ -114,6 +115,7 @@ export default function Environment() {
|
||||
DISABLE_MAGIC_LINK_LOGIN: false,
|
||||
DISABLE_EMAIL_VERIFICATION: false,
|
||||
DISABLE_BASIC_AUTHENTICATION: false,
|
||||
DISABLE_SIGN_UP: false,
|
||||
OLD_ADMIN_SECRET: '',
|
||||
DATABASE_NAME: '',
|
||||
DATABASE_TYPE: '',
|
||||
@@ -694,6 +696,18 @@ export default function Environment() {
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex>
|
||||
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||
<Text fontSize="sm">Disable Sign Up:</Text>
|
||||
</Flex>
|
||||
<Flex justifyContent="start" w="70%">
|
||||
<InputField
|
||||
variables={envVariables}
|
||||
setVariables={setEnvVariables}
|
||||
inputType={SwitchInputType.DISABLE_SIGN_UP}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Stack>
|
||||
<Divider marginTop="2%" marginBottom="2%" />
|
||||
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||
|
@@ -38,10 +38,11 @@ import {
|
||||
FaExclamationCircle,
|
||||
FaAngleDown,
|
||||
} from 'react-icons/fa';
|
||||
import { UserDetailsQuery } from '../graphql/queries';
|
||||
import { EmailVerificationQuery, UserDetailsQuery } from '../graphql/queries';
|
||||
import { UpdateUser } from '../graphql/mutation';
|
||||
import EditUserModal from '../components/EditUserModal';
|
||||
import DeleteUserModal from '../components/DeleteUserModal';
|
||||
import InviteMembersModal from '../components/InviteMembersModal';
|
||||
|
||||
interface paginationPropTypes {
|
||||
limit: number;
|
||||
@@ -101,6 +102,8 @@ export default function Users() {
|
||||
});
|
||||
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [disableInviteMembers, setDisableInviteMembers] =
|
||||
React.useState<boolean>(true);
|
||||
const updateUserList = async () => {
|
||||
setLoading(true);
|
||||
const { data } = await client
|
||||
@@ -132,8 +135,18 @@ export default function Users() {
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const checkEmailVerification = async () => {
|
||||
setLoading(true);
|
||||
const { data } = await client.query(EmailVerificationQuery).toPromise();
|
||||
if (data?._env) {
|
||||
const { DISABLE_EMAIL_VERIFICATION } = data._env;
|
||||
setDisableInviteMembers(DISABLE_EMAIL_VERIFICATION);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
React.useEffect(() => {
|
||||
updateUserList();
|
||||
checkEmailVerification();
|
||||
}, []);
|
||||
React.useEffect(() => {
|
||||
updateUserList();
|
||||
@@ -171,12 +184,17 @@ export default function Users() {
|
||||
}
|
||||
updateUserList();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
||||
<Text fontSize="md" fontWeight="bold">
|
||||
Users
|
||||
</Text>
|
||||
<InviteMembersModal
|
||||
disabled={disableInviteMembers}
|
||||
updateUserList={updateUserList}
|
||||
/>
|
||||
</Flex>
|
||||
{!loading ? (
|
||||
userList.length > 0 ? (
|
||||
|
@@ -64,3 +64,25 @@ export const getObjectDiff = (obj1: any, obj2: any) => {
|
||||
|
||||
return diff;
|
||||
};
|
||||
|
||||
export const validateEmail = (email: string) => {
|
||||
if (!email || email === '') return true;
|
||||
return email
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
||||
export const validateURI = (uri: string) => {
|
||||
if (!uri || uri === '') return true;
|
||||
return uri
|
||||
.toLowerCase()
|
||||
.match(
|
||||
/(?:^|\s)((https?:\/\/)?(?:localhost|[\w-]+(?:\.[\w-]+)+)(:\d+)?(\/\S*)?)/
|
||||
)
|
||||
? true
|
||||
: false;
|
||||
};
|
||||
|
39
dashboard/src/utils/parseCSV.ts
Normal file
39
dashboard/src/utils/parseCSV.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import _flatten from 'lodash/flatten';
|
||||
import { validateEmail } from '.';
|
||||
|
||||
interface dataTypes {
|
||||
value: string;
|
||||
isInvalid: boolean;
|
||||
}
|
||||
|
||||
const parseCSV = (file: File, delimiter: string): Promise<dataTypes[]> => {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
// When the FileReader has loaded the file...
|
||||
reader.onload = (e: any) => {
|
||||
// Split the result to an array of lines
|
||||
const lines = e.target.result.split('\n');
|
||||
// Split the lines themselves by the specified
|
||||
// delimiter, such as a comma
|
||||
let result = lines.map((line: string) => line.split(delimiter));
|
||||
// As the FileReader reads asynchronously,
|
||||
// we can't just return the result; instead,
|
||||
// we're passing it to a callback function
|
||||
result = _flatten(result);
|
||||
resolve(
|
||||
result.map((email: string) => {
|
||||
return {
|
||||
value: email.trim(),
|
||||
isInvalid: !validateEmail(email.trim()),
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Read the file content as a single string
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
export default parseCSV;
|
@@ -1,5 +1,7 @@
|
||||
package constants
|
||||
|
||||
var VERSION = "0.0.1"
|
||||
|
||||
const (
|
||||
// Envstore identifier
|
||||
// StringStore string store identifier
|
||||
@@ -13,8 +15,6 @@ const (
|
||||
EnvKeyEnv = "ENV"
|
||||
// EnvKeyEnvPath key for cli arg variable ENV_PATH
|
||||
EnvKeyEnvPath = "ENV_PATH"
|
||||
// EnvKeyVersion key for build arg version
|
||||
EnvKeyVersion = "VERSION"
|
||||
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
|
||||
// TODO: remove support AUTHORIZER_URL env
|
||||
EnvKeyAuthorizerURL = "AUTHORIZER_URL"
|
||||
@@ -67,6 +67,8 @@ const (
|
||||
EnvKeyDisableMagicLinkLogin = "DISABLE_MAGIC_LINK_LOGIN"
|
||||
// EnvKeyDisableLoginPage key for env variable DISABLE_LOGIN_PAGE
|
||||
EnvKeyDisableLoginPage = "DISABLE_LOGIN_PAGE"
|
||||
// EnvKeyDisableSignUp key for env variable DISABLE_SIGN_UP
|
||||
EnvKeyDisableSignUp = "DISABLE_SIGN_UP"
|
||||
// EnvKeyRoles key for env variable ROLES
|
||||
EnvKeyRoles = "ROLES"
|
||||
// EnvKeyProtectedRoles key for env variable PROTECTED_ROLES
|
||||
|
@@ -3,12 +3,14 @@ package crypto
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
)
|
||||
|
||||
var bytes = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 05}
|
||||
var bytes = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 0o5}
|
||||
|
||||
// EncryptAES method is to encrypt or hide any classified text
|
||||
func EncryptAES(text string) (string, error) {
|
||||
@@ -40,3 +42,67 @@ func DecryptAES(text string) (string, error) {
|
||||
cfb.XORKeyStream(plainText, []byte(cipherText))
|
||||
return string(plainText), nil
|
||||
}
|
||||
|
||||
// EncryptAESEnv encrypts data using AES algorithm
|
||||
// kept for the backward compatibility of env data encryption
|
||||
func EncryptAESEnv(text []byte) ([]byte, error) {
|
||||
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||
c, err := aes.NewCipher(key)
|
||||
var res []byte
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// gcm or Galois/Counter Mode, is a mode of operation
|
||||
// for symmetric key cryptographic block ciphers
|
||||
// - https://en.wikipedia.org/wiki/Galois/Counter_Mode
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// creates a new byte array the size of the nonce
|
||||
// which must be passed to Seal
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
// populates our nonce with a cryptographically secure
|
||||
// random sequence
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// here we encrypt our text using the Seal function
|
||||
// Seal encrypts and authenticates plaintext, authenticates the
|
||||
// additional data and appends the result to dst, returning the updated
|
||||
// slice. The nonce must be NonceSize() bytes long and unique for all
|
||||
// time, for a given key.
|
||||
return gcm.Seal(nonce, nonce, text, nil), nil
|
||||
}
|
||||
|
||||
// DecryptAES decrypts data using AES algorithm
|
||||
// Kept for the backward compatibility of env data decryption
|
||||
func DecryptAESEnv(ciphertext []byte) ([]byte, error) {
|
||||
key := []byte(envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyEncryptionKey))
|
||||
c, err := aes.NewCipher(key)
|
||||
var res []byte
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return res, err
|
||||
}
|
||||
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
@@ -94,12 +94,13 @@ func EncryptEnvData(data envstore.Store) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
encryptedConfig, err := EncryptAES(string(configData))
|
||||
|
||||
encryptedConfig, err := EncryptAESEnv(configData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(encryptedConfig), nil
|
||||
return EncryptB64(string(encryptedConfig)), nil
|
||||
}
|
||||
|
||||
// EncryptPassword is used for encrypting password
|
||||
|
@@ -32,6 +32,9 @@ type User struct {
|
||||
func (user *User) AsAPIUser() *model.User {
|
||||
isEmailVerified := user.EmailVerifiedAt != nil
|
||||
isPhoneVerified := user.PhoneNumberVerifiedAt != nil
|
||||
email := user.Email
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
return &model.User{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
@@ -41,14 +44,14 @@ func (user *User) AsAPIUser() *model.User {
|
||||
FamilyName: user.FamilyName,
|
||||
MiddleName: user.MiddleName,
|
||||
Nickname: user.Nickname,
|
||||
PreferredUsername: &user.Email,
|
||||
PreferredUsername: &email,
|
||||
Gender: user.Gender,
|
||||
Birthdate: user.Birthdate,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
PhoneNumberVerified: &isPhoneVerified,
|
||||
Picture: user.Picture,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
}
|
||||
|
@@ -4,25 +4,36 @@ import "github.com/authorizerdev/authorizer/server/graph/model"
|
||||
|
||||
// VerificationRequest model for db
|
||||
type VerificationRequest struct {
|
||||
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||
Token string `gorm:"type:text" json:"token" bson:"token"`
|
||||
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
|
||||
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||
Nonce string `gorm:"type:char(36)" json:"nonce" bson:"nonce"`
|
||||
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||
Token string `gorm:"type:text" json:"token" bson:"token"`
|
||||
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
|
||||
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
|
||||
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||
Nonce string `gorm:"type:text" json:"nonce" bson:"nonce"`
|
||||
RedirectURI string `gorm:"type:text" json:"redirect_uri" bson:"redirect_uri"`
|
||||
}
|
||||
|
||||
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
||||
token := v.Token
|
||||
createdAt := v.CreatedAt
|
||||
updatedAt := v.UpdatedAt
|
||||
email := v.Email
|
||||
nonce := v.Nonce
|
||||
redirectURI := v.RedirectURI
|
||||
expires := v.ExpiresAt
|
||||
identifier := v.Identifier
|
||||
return &model.VerificationRequest{
|
||||
ID: v.ID,
|
||||
Token: &v.Token,
|
||||
Identifier: &v.Identifier,
|
||||
Expires: &v.ExpiresAt,
|
||||
CreatedAt: &v.CreatedAt,
|
||||
UpdatedAt: &v.UpdatedAt,
|
||||
Email: &v.Email,
|
||||
ID: v.ID,
|
||||
Token: &token,
|
||||
Identifier: &identifier,
|
||||
Expires: &expires,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
Email: &email,
|
||||
Nonce: &nonce,
|
||||
RedirectURI: &redirectURI,
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
|
||||
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||
result := p.db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
|
||||
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "nonce", "redirect_uri"}),
|
||||
}).Create(&verificationRequest)
|
||||
|
||||
if result.Error != nil {
|
||||
|
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,6 +1,8 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
)
|
||||
@@ -103,5 +105,9 @@ func SendVerificationMail(toEmail, token, hostname string) error {
|
||||
message = addEmailTemplate(message, data, "verify_email.tmpl")
|
||||
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
|
||||
|
||||
return SendMail(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
@@ -281,6 +281,7 @@ func InitAllEnv() error {
|
||||
envData.BoolEnv[constants.EnvKeyDisableEmailVerification] = os.Getenv(constants.EnvKeyDisableEmailVerification) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableMagicLinkLogin] = os.Getenv(constants.EnvKeyDisableMagicLinkLogin) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableLoginPage] = os.Getenv(constants.EnvKeyDisableLoginPage) == "true"
|
||||
envData.BoolEnv[constants.EnvKeyDisableSignUp] = os.Getenv(constants.EnvKeyDisableSignUp) == "true"
|
||||
|
||||
// no need to add nil check as its already done above
|
||||
if envData.StringEnv[constants.EnvKeySmtpHost] == "" || envData.StringEnv[constants.EnvKeySmtpUsername] == "" || envData.StringEnv[constants.EnvKeySmtpPassword] == "" || envData.StringEnv[constants.EnvKeySenderEmail] == "" && envData.StringEnv[constants.EnvKeySmtpPort] == "" {
|
||||
|
20
server/env/persist_env.go
vendored
20
server/env/persist_env.go
vendored
@@ -34,12 +34,17 @@ func GetEnvData() (envstore.Store, error) {
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey)
|
||||
|
||||
decryptedConfigs, err := crypto.DecryptAES(env.EnvData)
|
||||
b64DecryptedConfig, err := crypto.DecryptB64(env.EnvData)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(decryptedConfigs), &result)
|
||||
decryptedConfigs, err := crypto.DecryptAESEnv([]byte(b64DecryptedConfig))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(decryptedConfigs, &result)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -82,7 +87,12 @@ func PersistEnv() error {
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey)
|
||||
|
||||
decryptedConfigs, err := crypto.DecryptAES(env.EnvData)
|
||||
b64DecryptedConfig, err := crypto.DecryptB64(env.EnvData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decryptedConfigs, err := crypto.DecryptAESEnv([]byte(b64DecryptedConfig))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -90,7 +100,7 @@ func PersistEnv() error {
|
||||
// temp store variable
|
||||
var storeData envstore.Store
|
||||
|
||||
err = json.Unmarshal([]byte(decryptedConfigs), &storeData)
|
||||
err = json.Unmarshal(decryptedConfigs, &storeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,7 +112,7 @@ func PersistEnv() error {
|
||||
|
||||
for key, value := range storeData.StringEnv {
|
||||
// don't override unexposed envs
|
||||
if key != constants.EnvKeyEncryptionKey && key != constants.EnvKeyClientID && key != constants.EnvKeyClientSecret && key != constants.EnvKeyJWK {
|
||||
if key != constants.EnvKeyEncryptionKey {
|
||||
// check only for derivative keys
|
||||
// No need to check for ENCRYPTION_KEY which special key we use for encrypting config data
|
||||
// as we have removed it from json
|
||||
|
@@ -41,6 +41,7 @@ var defaultStore = &EnvStore{
|
||||
constants.EnvKeyDisableMagicLinkLogin: false,
|
||||
constants.EnvKeyDisableEmailVerification: false,
|
||||
constants.EnvKeyDisableLoginPage: false,
|
||||
constants.EnvKeyDisableSignUp: false,
|
||||
},
|
||||
SliceEnv: map[string][]string{},
|
||||
},
|
||||
|
@@ -68,6 +68,7 @@ type ComplexityRoot struct {
|
||||
DisableEmailVerification func(childComplexity int) int
|
||||
DisableLoginPage func(childComplexity int) int
|
||||
DisableMagicLinkLogin func(childComplexity int) int
|
||||
DisableSignUp func(childComplexity int) int
|
||||
FacebookClientID func(childComplexity int) int
|
||||
FacebookClientSecret func(childComplexity int) int
|
||||
GithubClientID func(childComplexity int) int
|
||||
@@ -105,6 +106,7 @@ type ComplexityRoot struct {
|
||||
IsGithubLoginEnabled func(childComplexity int) int
|
||||
IsGoogleLoginEnabled func(childComplexity int) int
|
||||
IsMagicLinkLoginEnabled func(childComplexity int) int
|
||||
IsSignUpEnabled func(childComplexity int) int
|
||||
Version func(childComplexity int) int
|
||||
}
|
||||
|
||||
@@ -114,11 +116,13 @@ type ComplexityRoot struct {
|
||||
AdminSignup func(childComplexity int, params model.AdminSignupInput) int
|
||||
DeleteUser func(childComplexity int, params model.DeleteUserInput) int
|
||||
ForgotPassword func(childComplexity int, params model.ForgotPasswordInput) int
|
||||
InviteMembers func(childComplexity int, params model.InviteMemberInput) int
|
||||
Login func(childComplexity int, params model.LoginInput) int
|
||||
Logout func(childComplexity int) int
|
||||
MagicLinkLogin func(childComplexity int, params model.MagicLinkLoginInput) int
|
||||
ResendVerifyEmail func(childComplexity int, params model.ResendVerifyEmailInput) int
|
||||
ResetPassword func(childComplexity int, params model.ResetPasswordInput) int
|
||||
Revoke func(childComplexity int, params model.OAuthRevokeInput) int
|
||||
Signup func(childComplexity int, params model.SignUpInput) int
|
||||
UpdateEnv func(childComplexity int, params model.UpdateEnvInput) int
|
||||
UpdateProfile func(childComplexity int, params model.UpdateProfileInput) int
|
||||
@@ -173,13 +177,15 @@ type ComplexityRoot struct {
|
||||
}
|
||||
|
||||
VerificationRequest struct {
|
||||
CreatedAt func(childComplexity int) int
|
||||
Email func(childComplexity int) int
|
||||
Expires func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
Identifier func(childComplexity int) int
|
||||
Token func(childComplexity int) int
|
||||
UpdatedAt func(childComplexity int) int
|
||||
CreatedAt func(childComplexity int) int
|
||||
Email func(childComplexity int) int
|
||||
Expires func(childComplexity int) int
|
||||
ID func(childComplexity int) int
|
||||
Identifier func(childComplexity int) int
|
||||
Nonce func(childComplexity int) int
|
||||
RedirectURI func(childComplexity int) int
|
||||
Token func(childComplexity int) int
|
||||
UpdatedAt func(childComplexity int) int
|
||||
}
|
||||
|
||||
VerificationRequests struct {
|
||||
@@ -198,12 +204,14 @@ type MutationResolver interface {
|
||||
ResendVerifyEmail(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error)
|
||||
ForgotPassword(ctx context.Context, params model.ForgotPasswordInput) (*model.Response, error)
|
||||
ResetPassword(ctx context.Context, params model.ResetPasswordInput) (*model.Response, error)
|
||||
Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error)
|
||||
DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error)
|
||||
UpdateUser(ctx context.Context, params model.UpdateUserInput) (*model.User, error)
|
||||
AdminSignup(ctx context.Context, params model.AdminSignupInput) (*model.Response, error)
|
||||
AdminLogin(ctx context.Context, params model.AdminLoginInput) (*model.Response, error)
|
||||
AdminLogout(ctx context.Context) (*model.Response, error)
|
||||
UpdateEnv(ctx context.Context, params model.UpdateEnvInput) (*model.Response, error)
|
||||
InviteMembers(ctx context.Context, params model.InviteMemberInput) (*model.Response, error)
|
||||
}
|
||||
type QueryResolver interface {
|
||||
Meta(ctx context.Context) (*model.Meta, error)
|
||||
@@ -377,6 +385,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Env.DisableMagicLinkLogin(childComplexity), true
|
||||
|
||||
case "Env.DISABLE_SIGN_UP":
|
||||
if e.complexity.Env.DisableSignUp == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Env.DisableSignUp(childComplexity), true
|
||||
|
||||
case "Env.FACEBOOK_CLIENT_ID":
|
||||
if e.complexity.Env.FacebookClientID == nil {
|
||||
break
|
||||
@@ -594,6 +609,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Meta.IsMagicLinkLoginEnabled(childComplexity), true
|
||||
|
||||
case "Meta.is_sign_up_enabled":
|
||||
if e.complexity.Meta.IsSignUpEnabled == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.Meta.IsSignUpEnabled(childComplexity), true
|
||||
|
||||
case "Meta.version":
|
||||
if e.complexity.Meta.Version == nil {
|
||||
break
|
||||
@@ -656,6 +678,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.ForgotPassword(childComplexity, args["params"].(model.ForgotPasswordInput)), true
|
||||
|
||||
case "Mutation._invite_members":
|
||||
if e.complexity.Mutation.InviteMembers == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation__invite_members_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.InviteMembers(childComplexity, args["params"].(model.InviteMemberInput)), true
|
||||
|
||||
case "Mutation.login":
|
||||
if e.complexity.Mutation.Login == nil {
|
||||
break
|
||||
@@ -711,6 +745,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.Mutation.ResetPassword(childComplexity, args["params"].(model.ResetPasswordInput)), true
|
||||
|
||||
case "Mutation.revoke":
|
||||
if e.complexity.Mutation.Revoke == nil {
|
||||
break
|
||||
}
|
||||
|
||||
args, err := ec.field_Mutation_revoke_args(context.TODO(), rawArgs)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return e.complexity.Mutation.Revoke(childComplexity, args["params"].(model.OAuthRevokeInput)), true
|
||||
|
||||
case "Mutation.signup":
|
||||
if e.complexity.Mutation.Signup == nil {
|
||||
break
|
||||
@@ -1038,6 +1084,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||
|
||||
return e.complexity.VerificationRequest.Identifier(childComplexity), true
|
||||
|
||||
case "VerificationRequest.nonce":
|
||||
if e.complexity.VerificationRequest.Nonce == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.VerificationRequest.Nonce(childComplexity), true
|
||||
|
||||
case "VerificationRequest.redirect_uri":
|
||||
if e.complexity.VerificationRequest.RedirectURI == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.VerificationRequest.RedirectURI(childComplexity), true
|
||||
|
||||
case "VerificationRequest.token":
|
||||
if e.complexity.VerificationRequest.Token == nil {
|
||||
break
|
||||
@@ -1153,6 +1213,7 @@ type Meta {
|
||||
is_email_verification_enabled: Boolean!
|
||||
is_basic_authentication_enabled: Boolean!
|
||||
is_magic_link_login_enabled: Boolean!
|
||||
is_sign_up_enabled: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
@@ -1189,6 +1250,8 @@ type VerificationRequest {
|
||||
expires: Int64
|
||||
created_at: Int64
|
||||
updated_at: Int64
|
||||
nonce: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
type VerificationRequests {
|
||||
@@ -1240,6 +1303,7 @@ type Env {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -1276,6 +1340,7 @@ input UpdateEnvInput {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -1311,6 +1376,8 @@ input SignUpInput {
|
||||
password: String!
|
||||
confirm_password: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
@@ -1361,6 +1428,8 @@ input UpdateUserInput {
|
||||
|
||||
input ForgotPasswordInput {
|
||||
email: String!
|
||||
state: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input ResetPasswordInput {
|
||||
@@ -1377,6 +1446,8 @@ input MagicLinkLoginInput {
|
||||
email: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
state: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input SessionQueryInput {
|
||||
@@ -1393,6 +1464,15 @@ input PaginatedInput {
|
||||
pagination: PaginationInput
|
||||
}
|
||||
|
||||
input OAuthRevokeInput {
|
||||
refresh_token: String!
|
||||
}
|
||||
|
||||
input InviteMemberInput {
|
||||
emails: [String!]!
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signup(params: SignUpInput!): AuthResponse!
|
||||
login(params: LoginInput!): AuthResponse!
|
||||
@@ -1403,6 +1483,7 @@ type Mutation {
|
||||
resend_verify_email(params: ResendVerifyEmailInput!): Response!
|
||||
forgot_password(params: ForgotPasswordInput!): Response!
|
||||
reset_password(params: ResetPasswordInput!): Response!
|
||||
revoke(params: OAuthRevokeInput!): Response!
|
||||
# admin only apis
|
||||
_delete_user(params: DeleteUserInput!): Response!
|
||||
_update_user(params: UpdateUserInput!): User!
|
||||
@@ -1410,6 +1491,7 @@ type Mutation {
|
||||
_admin_login(params: AdminLoginInput!): Response!
|
||||
_admin_logout: Response!
|
||||
_update_env(params: UpdateEnvInput!): Response!
|
||||
_invite_members(params: InviteMemberInput!): Response!
|
||||
}
|
||||
|
||||
type Query {
|
||||
@@ -1475,6 +1557,21 @@ func (ec *executionContext) field_Mutation__delete_user_args(ctx context.Context
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation__invite_members_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 model.InviteMemberInput
|
||||
if tmp, ok := rawArgs["params"]; ok {
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
|
||||
arg0, err = ec.unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["params"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation__update_env_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@@ -1580,6 +1677,21 @@ func (ec *executionContext) field_Mutation_reset_password_args(ctx context.Conte
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_revoke_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
var arg0 model.OAuthRevokeInput
|
||||
if tmp, ok := rawArgs["params"]; ok {
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("params"))
|
||||
arg0, err = ec.unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx, tmp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
args["params"] = arg0
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) field_Mutation_signup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
args := map[string]interface{}{}
|
||||
@@ -2733,6 +2845,38 @@ func (ec *executionContext) _Env_DISABLE_LOGIN_PAGE(ctx context.Context, field g
|
||||
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Env_DISABLE_SIGN_UP(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Env",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.DisableSignUp, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*bool)
|
||||
fc.Result = res
|
||||
return ec.marshalOBoolean2ᚖbool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Env_ROLES(ctx context.Context, field graphql.CollectedField, obj *model.Env) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -3467,6 +3611,41 @@ func (ec *executionContext) _Meta_is_magic_link_login_enabled(ctx context.Contex
|
||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Meta_is_sign_up_enabled(ctx context.Context, field graphql.CollectedField, obj *model.Meta) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Meta",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.IsSignUpEnabled, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(bool)
|
||||
fc.Result = res
|
||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_signup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -3838,6 +4017,48 @@ func (ec *executionContext) _Mutation_reset_password(ctx context.Context, field
|
||||
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation_revoke(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Mutation_revoke_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().Revoke(rctx, args["params"].(model.OAuthRevokeInput))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*model.Response)
|
||||
fc.Result = res
|
||||
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation__delete_user(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -4083,6 +4304,48 @@ func (ec *executionContext) _Mutation__update_env(ctx context.Context, field gra
|
||||
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Mutation__invite_members(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "Mutation",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: true,
|
||||
IsResolver: true,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
rawArgs := field.ArgumentMap(ec.Variables)
|
||||
args, err := ec.field_Mutation__invite_members_args(ctx, rawArgs)
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
fc.Args = args
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return ec.resolvers.Mutation().InviteMembers(rctx, args["params"].(model.InviteMemberInput))
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
if !graphql.HasFieldError(ctx, fc) {
|
||||
ec.Errorf(ctx, "must not be null")
|
||||
}
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*model.Response)
|
||||
fc.Result = res
|
||||
return ec.marshalNResponse2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐResponse(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _Pagination_limit(ctx context.Context, field graphql.CollectedField, obj *model.Pagination) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -5451,6 +5714,70 @@ func (ec *executionContext) _VerificationRequest_updated_at(ctx context.Context,
|
||||
return ec.marshalOInt642ᚖint64(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _VerificationRequest_nonce(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "VerificationRequest",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.Nonce, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _VerificationRequest_redirect_uri(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequest) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "VerificationRequest",
|
||||
Field: field,
|
||||
Args: nil,
|
||||
IsMethod: false,
|
||||
IsResolver: false,
|
||||
}
|
||||
|
||||
ctx = graphql.WithFieldContext(ctx, fc)
|
||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||
ctx = rctx // use context from middleware stack in children
|
||||
return obj.RedirectURI, nil
|
||||
})
|
||||
if err != nil {
|
||||
ec.Error(ctx, err)
|
||||
return graphql.Null
|
||||
}
|
||||
if resTmp == nil {
|
||||
return graphql.Null
|
||||
}
|
||||
res := resTmp.(*string)
|
||||
fc.Result = res
|
||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _VerificationRequests_pagination(ctx context.Context, field graphql.CollectedField, obj *model.VerificationRequests) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -6729,6 +7056,53 @@ func (ec *executionContext) unmarshalInputForgotPasswordInput(ctx context.Contex
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "state":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
|
||||
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "redirect_uri":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
|
||||
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputInviteMemberInput(ctx context.Context, obj interface{}) (model.InviteMemberInput, error) {
|
||||
var it model.InviteMemberInput
|
||||
asMap := map[string]interface{}{}
|
||||
for k, v := range obj.(map[string]interface{}) {
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "emails":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emails"))
|
||||
it.Emails, err = ec.unmarshalNString2ᚕstringᚄ(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "redirect_uri":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
|
||||
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6815,6 +7189,45 @@ func (ec *executionContext) unmarshalInputMagicLinkLoginInput(ctx context.Contex
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "state":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("state"))
|
||||
it.State, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "redirect_uri":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
|
||||
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return it, nil
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalInputOAuthRevokeInput(ctx context.Context, obj interface{}) (model.OAuthRevokeInput, error) {
|
||||
var it model.OAuthRevokeInput
|
||||
asMap := map[string]interface{}{}
|
||||
for k, v := range obj.(map[string]interface{}) {
|
||||
asMap[k] = v
|
||||
}
|
||||
|
||||
for k, v := range asMap {
|
||||
switch k {
|
||||
case "refresh_token":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("refresh_token"))
|
||||
it.RefreshToken, err = ec.unmarshalNString2string(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7081,6 +7494,22 @@ func (ec *executionContext) unmarshalInputSignUpInput(ctx context.Context, obj i
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "scope":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("scope"))
|
||||
it.Scope, err = ec.unmarshalOString2ᚕstringᚄ(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "redirect_uri":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("redirect_uri"))
|
||||
it.RedirectURI, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7264,6 +7693,14 @@ func (ec *executionContext) unmarshalInputUpdateEnvInput(ctx context.Context, ob
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "DISABLE_SIGN_UP":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("DISABLE_SIGN_UP"))
|
||||
it.DisableSignUp, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "ROLES":
|
||||
var err error
|
||||
|
||||
@@ -7732,6 +8169,8 @@ func (ec *executionContext) _Env(ctx context.Context, sel ast.SelectionSet, obj
|
||||
out.Values[i] = ec._Env_DISABLE_MAGIC_LINK_LOGIN(ctx, field, obj)
|
||||
case "DISABLE_LOGIN_PAGE":
|
||||
out.Values[i] = ec._Env_DISABLE_LOGIN_PAGE(ctx, field, obj)
|
||||
case "DISABLE_SIGN_UP":
|
||||
out.Values[i] = ec._Env_DISABLE_SIGN_UP(ctx, field, obj)
|
||||
case "ROLES":
|
||||
out.Values[i] = ec._Env_ROLES(ctx, field, obj)
|
||||
case "PROTECTED_ROLES":
|
||||
@@ -7850,6 +8289,11 @@ func (ec *executionContext) _Meta(ctx context.Context, sel ast.SelectionSet, obj
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "is_sign_up_enabled":
|
||||
out.Values[i] = ec._Meta_is_sign_up_enabled(ctx, field, obj)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@@ -7921,6 +8365,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "revoke":
|
||||
out.Values[i] = ec._Mutation_revoke(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "_delete_user":
|
||||
out.Values[i] = ec._Mutation__delete_user(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
@@ -7951,6 +8400,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "_invite_members":
|
||||
out.Values[i] = ec._Mutation__invite_members(ctx, field)
|
||||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@@ -8290,6 +8744,10 @@ func (ec *executionContext) _VerificationRequest(ctx context.Context, sel ast.Se
|
||||
out.Values[i] = ec._VerificationRequest_created_at(ctx, field, obj)
|
||||
case "updated_at":
|
||||
out.Values[i] = ec._VerificationRequest_updated_at(ctx, field, obj)
|
||||
case "nonce":
|
||||
out.Values[i] = ec._VerificationRequest_nonce(ctx, field, obj)
|
||||
case "redirect_uri":
|
||||
out.Values[i] = ec._VerificationRequest_redirect_uri(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
@@ -8676,6 +9134,11 @@ func (ec *executionContext) marshalNInt642int64(ctx context.Context, sel ast.Sel
|
||||
return res
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNInviteMemberInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐInviteMemberInput(ctx context.Context, v interface{}) (model.InviteMemberInput, error) {
|
||||
res, err := ec.unmarshalInputInviteMemberInput(ctx, v)
|
||||
return res, graphql.ErrorOnPath(ctx, err)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNLoginInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐLoginInput(ctx context.Context, v interface{}) (model.LoginInput, error) {
|
||||
res, err := ec.unmarshalInputLoginInput(ctx, v)
|
||||
return res, graphql.ErrorOnPath(ctx, err)
|
||||
@@ -8700,6 +9163,11 @@ func (ec *executionContext) marshalNMeta2ᚖgithubᚗcomᚋauthorizerdevᚋautho
|
||||
return ec._Meta(ctx, sel, v)
|
||||
}
|
||||
|
||||
func (ec *executionContext) unmarshalNOAuthRevokeInput2githubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐOAuthRevokeInput(ctx context.Context, v interface{}) (model.OAuthRevokeInput, error) {
|
||||
res, err := ec.unmarshalInputOAuthRevokeInput(ctx, v)
|
||||
return res, graphql.ErrorOnPath(ctx, err)
|
||||
}
|
||||
|
||||
func (ec *executionContext) marshalNPagination2ᚖgithubᚗcomᚋauthorizerdevᚋauthorizerᚋserverᚋgraphᚋmodelᚐPagination(ctx context.Context, sel ast.SelectionSet, v *model.Pagination) graphql.Marshaler {
|
||||
if v == nil {
|
||||
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
|
||||
|
@@ -49,6 +49,7 @@ type Env struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -69,7 +70,14 @@ type Error struct {
|
||||
}
|
||||
|
||||
type ForgotPasswordInput struct {
|
||||
Email string `json:"email"`
|
||||
Email string `json:"email"`
|
||||
State *string `json:"state"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type InviteMemberInput struct {
|
||||
Emails []string `json:"emails"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type LoginInput struct {
|
||||
@@ -80,9 +88,11 @@ type LoginInput struct {
|
||||
}
|
||||
|
||||
type MagicLinkLoginInput struct {
|
||||
Email string `json:"email"`
|
||||
Roles []string `json:"roles"`
|
||||
Scope []string `json:"scope"`
|
||||
Email string `json:"email"`
|
||||
Roles []string `json:"roles"`
|
||||
Scope []string `json:"scope"`
|
||||
State *string `json:"state"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
@@ -94,6 +104,11 @@ type Meta struct {
|
||||
IsEmailVerificationEnabled bool `json:"is_email_verification_enabled"`
|
||||
IsBasicAuthenticationEnabled bool `json:"is_basic_authentication_enabled"`
|
||||
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
||||
IsSignUpEnabled bool `json:"is_sign_up_enabled"`
|
||||
}
|
||||
|
||||
type OAuthRevokeInput struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type PaginatedInput struct {
|
||||
@@ -145,6 +160,8 @@ type SignUpInput struct {
|
||||
Password string `json:"password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
Roles []string `json:"roles"`
|
||||
Scope []string `json:"scope"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type UpdateEnvInput struct {
|
||||
@@ -169,6 +186,7 @@ type UpdateEnvInput struct {
|
||||
DisableBasicAuthentication *bool `json:"DISABLE_BASIC_AUTHENTICATION"`
|
||||
DisableMagicLinkLogin *bool `json:"DISABLE_MAGIC_LINK_LOGIN"`
|
||||
DisableLoginPage *bool `json:"DISABLE_LOGIN_PAGE"`
|
||||
DisableSignUp *bool `json:"DISABLE_SIGN_UP"`
|
||||
Roles []string `json:"ROLES"`
|
||||
ProtectedRoles []string `json:"PROTECTED_ROLES"`
|
||||
DefaultRoles []string `json:"DEFAULT_ROLES"`
|
||||
@@ -239,13 +257,15 @@ type Users struct {
|
||||
}
|
||||
|
||||
type VerificationRequest struct {
|
||||
ID string `json:"id"`
|
||||
Identifier *string `json:"identifier"`
|
||||
Token *string `json:"token"`
|
||||
Email *string `json:"email"`
|
||||
Expires *int64 `json:"expires"`
|
||||
CreatedAt *int64 `json:"created_at"`
|
||||
UpdatedAt *int64 `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Identifier *string `json:"identifier"`
|
||||
Token *string `json:"token"`
|
||||
Email *string `json:"email"`
|
||||
Expires *int64 `json:"expires"`
|
||||
CreatedAt *int64 `json:"created_at"`
|
||||
UpdatedAt *int64 `json:"updated_at"`
|
||||
Nonce *string `json:"nonce"`
|
||||
RedirectURI *string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
type VerificationRequests struct {
|
||||
|
@@ -21,6 +21,7 @@ type Meta {
|
||||
is_email_verification_enabled: Boolean!
|
||||
is_basic_authentication_enabled: Boolean!
|
||||
is_magic_link_login_enabled: Boolean!
|
||||
is_sign_up_enabled: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
@@ -57,6 +58,8 @@ type VerificationRequest {
|
||||
expires: Int64
|
||||
created_at: Int64
|
||||
updated_at: Int64
|
||||
nonce: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
type VerificationRequests {
|
||||
@@ -108,6 +111,7 @@ type Env {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -144,6 +148,7 @@ input UpdateEnvInput {
|
||||
DISABLE_BASIC_AUTHENTICATION: Boolean
|
||||
DISABLE_MAGIC_LINK_LOGIN: Boolean
|
||||
DISABLE_LOGIN_PAGE: Boolean
|
||||
DISABLE_SIGN_UP: Boolean
|
||||
ROLES: [String!]
|
||||
PROTECTED_ROLES: [String!]
|
||||
DEFAULT_ROLES: [String!]
|
||||
@@ -179,6 +184,8 @@ input SignUpInput {
|
||||
password: String!
|
||||
confirm_password: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input LoginInput {
|
||||
@@ -229,6 +236,8 @@ input UpdateUserInput {
|
||||
|
||||
input ForgotPasswordInput {
|
||||
email: String!
|
||||
state: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input ResetPasswordInput {
|
||||
@@ -245,6 +254,8 @@ input MagicLinkLoginInput {
|
||||
email: String!
|
||||
roles: [String!]
|
||||
scope: [String!]
|
||||
state: String
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
input SessionQueryInput {
|
||||
@@ -261,6 +272,15 @@ input PaginatedInput {
|
||||
pagination: PaginationInput
|
||||
}
|
||||
|
||||
input OAuthRevokeInput {
|
||||
refresh_token: String!
|
||||
}
|
||||
|
||||
input InviteMemberInput {
|
||||
emails: [String!]!
|
||||
redirect_uri: String
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signup(params: SignUpInput!): AuthResponse!
|
||||
login(params: LoginInput!): AuthResponse!
|
||||
@@ -271,6 +291,7 @@ type Mutation {
|
||||
resend_verify_email(params: ResendVerifyEmailInput!): Response!
|
||||
forgot_password(params: ForgotPasswordInput!): Response!
|
||||
reset_password(params: ResetPasswordInput!): Response!
|
||||
revoke(params: OAuthRevokeInput!): Response!
|
||||
# admin only apis
|
||||
_delete_user(params: DeleteUserInput!): Response!
|
||||
_update_user(params: UpdateUserInput!): User!
|
||||
@@ -278,6 +299,7 @@ type Mutation {
|
||||
_admin_login(params: AdminLoginInput!): Response!
|
||||
_admin_logout: Response!
|
||||
_update_env(params: UpdateEnvInput!): Response!
|
||||
_invite_members(params: InviteMemberInput!): Response!
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
@@ -47,6 +47,10 @@ func (r *mutationResolver) ResetPassword(ctx context.Context, params model.Reset
|
||||
return resolvers.ResetPasswordResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) Revoke(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
|
||||
return resolvers.RevokeResolver(ctx, params)
|
||||
}
|
||||
|
||||
func (r *mutationResolver) DeleteUser(ctx context.Context, params model.DeleteUserInput) (*model.Response, error) {
|
||||
return resolvers.DeleteUserResolver(ctx, params)
|
||||
}
|
||||
@@ -71,6 +75,10 @@ func (r *mutationResolver) UpdateEnv(ctx context.Context, params model.UpdateEnv
|
||||
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 *queryResolver) Meta(ctx context.Context) (*model.Meta, error) {
|
||||
return resolvers.MetaResolver(ctx)
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -29,44 +27,25 @@ func AppHandler() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
state := c.Query("state")
|
||||
redirect_uri := strings.TrimSpace(c.Query("redirect_uri"))
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||
|
||||
var stateObj State
|
||||
|
||||
if state == "" {
|
||||
stateObj.AuthorizerURL = hostname
|
||||
stateObj.RedirectURL = hostname + "/app"
|
||||
var scope []string
|
||||
if scopeString == "" {
|
||||
scope = []string{"openid", "profile", "email"}
|
||||
} else {
|
||||
decodedState, err := crypto.DecryptB64(state)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "[unable to decode state] invalid state"})
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal([]byte(decodedState), &stateObj)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{"error": "[unable to parse state] invalid state"})
|
||||
return
|
||||
}
|
||||
stateObj.AuthorizerURL = strings.TrimSuffix(stateObj.AuthorizerURL, "/")
|
||||
stateObj.RedirectURL = strings.TrimSuffix(stateObj.RedirectURL, "/")
|
||||
scope = strings.Split(scopeString, " ")
|
||||
}
|
||||
|
||||
if redirect_uri == "" {
|
||||
redirect_uri = hostname + "/app"
|
||||
} else {
|
||||
// validate redirect url with allowed origins
|
||||
if !utils.IsValidOrigin(stateObj.RedirectURL) {
|
||||
if !utils.IsValidOrigin(redirect_uri) {
|
||||
c.JSON(400, gin.H{"error": "invalid redirect url"})
|
||||
return
|
||||
}
|
||||
|
||||
if stateObj.AuthorizerURL == "" {
|
||||
c.JSON(400, gin.H{"error": "invalid authorizer url"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate host and domain of authorizer url
|
||||
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != hostname {
|
||||
c.JSON(400, gin.H{"error": "invalid host url"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// debug the request state
|
||||
@@ -77,9 +56,11 @@ func AppHandler() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
c.HTML(http.StatusOK, "app.tmpl", gin.H{
|
||||
"data": map[string]string{
|
||||
"authorizerURL": stateObj.AuthorizerURL,
|
||||
"redirectURL": stateObj.RedirectURL,
|
||||
"data": map[string]interface{}{
|
||||
"authorizerURL": hostname,
|
||||
"redirectURL": redirect_uri,
|
||||
"scope": scope,
|
||||
"state": state,
|
||||
"organizationName": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName),
|
||||
"organizationLogo": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo),
|
||||
},
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
// AuthorizeHandler is the handler for the /authorize route
|
||||
// required params
|
||||
// ?redirect_uri = redirect url
|
||||
// ?response_mode = to decide if result should be html or re-direct
|
||||
// state[recommended] = to prevent CSRF attack (for authorizer its compulsory)
|
||||
// code_challenge = to prevent CSRF attack
|
||||
// code_challenge_method = to prevent CSRF attack [only sh256 is supported]
|
||||
@@ -31,62 +34,7 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
scopeString := strings.TrimSpace(gc.Query("scope"))
|
||||
clientID := strings.TrimSpace(gc.Query("client_id"))
|
||||
template := "authorize.tmpl"
|
||||
|
||||
if clientID == "" {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "client_id is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "invalid_client_id",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if redirectURI == "" {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "redirect_uri is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if state == "" {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "state is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if responseType == "" {
|
||||
responseType = "token"
|
||||
}
|
||||
responseMode := strings.TrimSpace(gc.Query("response_mode"))
|
||||
|
||||
var scope []string
|
||||
if scopeString == "" {
|
||||
@@ -95,80 +43,173 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
scope = strings.Split(scopeString, " ")
|
||||
}
|
||||
|
||||
if responseMode == "" {
|
||||
responseMode = "query"
|
||||
}
|
||||
|
||||
if responseMode != "query" && responseMode != "web_message" {
|
||||
gc.JSON(400, gin.H{"error": "invalid response mode"})
|
||||
}
|
||||
|
||||
fmt.Println("=> redirect URI:", redirectURI)
|
||||
fmt.Println("=> state:", state)
|
||||
if redirectURI == "" {
|
||||
redirectURI = "/app"
|
||||
}
|
||||
|
||||
isQuery := responseMode == "query"
|
||||
|
||||
loginURL := "/app?state=" + state + "&scope=" + strings.Join(scope, " ") + "&redirect_uri=" + redirectURI
|
||||
|
||||
if clientID == "" {
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "client_id is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "invalid_client_id",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if state == "" {
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "state is required",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if responseType == "" {
|
||||
responseType = "token"
|
||||
}
|
||||
|
||||
isResponseTypeCode := responseType == "code"
|
||||
isResponseTypeToken := responseType == "token"
|
||||
|
||||
if !isResponseTypeCode && !isResponseTypeToken {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "response_type is invalid",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "response_type is invalid",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isResponseTypeCode {
|
||||
if codeChallenge == "" {
|
||||
gc.HTML(http.StatusBadRequest, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "code_challenge is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusBadRequest, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "code_challenge is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
sessionToken, err := cookie.GetSession(gc)
|
||||
if err != nil {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// get session from cookie
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionToken)
|
||||
if err != nil {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
userID := claims.Subject
|
||||
user, err := db.Provider.GetUserByID(userID)
|
||||
if err != nil {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "signup_required",
|
||||
"error_description": "Sign up required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "signup_required",
|
||||
"error_description": "Sign up required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,16 +221,20 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
nonce := uuid.New().String()
|
||||
newSessionTokenData, newSessionToken, err := token.CreateSessionToken(user, nonce, claims.Roles, scope)
|
||||
if err != nil {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -214,24 +259,31 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
// rollover the session for security
|
||||
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, scope)
|
||||
if err != nil {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
sessionstore.RemoveState(sessionToken)
|
||||
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
|
||||
// used of query mode
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
|
||||
|
||||
res := map[string]interface{}{
|
||||
"access_token": authToken.AccessToken.Token,
|
||||
"id_token": authToken.IDToken.Token,
|
||||
@@ -243,29 +295,42 @@ func AuthorizeHandler() gin.HandlerFunc {
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
res["refresh_token"] = authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
params += "&refresh_token=" + authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": res,
|
||||
},
|
||||
})
|
||||
if isQuery {
|
||||
if strings.Contains(redirectURI, "?") {
|
||||
gc.Redirect(http.StatusFound, redirectURI+"&"+params)
|
||||
} else {
|
||||
gc.Redirect(http.StatusFound, redirectURI+"?"+params)
|
||||
}
|
||||
} else {
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": res,
|
||||
},
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// by default return with error
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
if isQuery {
|
||||
gc.Redirect(http.StatusFound, loginURL)
|
||||
} else {
|
||||
// by default return with error
|
||||
gc.HTML(http.StatusOK, template, gin.H{
|
||||
"target_origin": redirectURI,
|
||||
"authorization_response": map[string]interface{}{
|
||||
"type": "authorization_response",
|
||||
"response": map[string]string{
|
||||
"error": "login_required",
|
||||
"error_description": "Login is required",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
"github.com/authorizerdev/authorizer/server/crypto"
|
||||
@@ -9,8 +10,10 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Handler to logout user
|
||||
func LogoutHandler() gin.HandlerFunc {
|
||||
return func(gc *gin.Context) {
|
||||
redirectURL := strings.TrimSpace(gc.Query("redirect_uri"))
|
||||
// get fingerprint hash
|
||||
fingerprintHash, err := cookie.GetSession(gc)
|
||||
if err != nil {
|
||||
@@ -33,8 +36,12 @@ func LogoutHandler() gin.HandlerFunc {
|
||||
sessionstore.RemoveState(fingerPrint)
|
||||
cookie.DeleteSession(gc)
|
||||
|
||||
gc.JSON(http.StatusOK, gin.H{
|
||||
"message": "Logged out successfully",
|
||||
})
|
||||
if redirectURL != "" {
|
||||
gc.Redirect(http.StatusFound, redirectURL)
|
||||
} else {
|
||||
gc.JSON(http.StatusOK, gin.H{
|
||||
"message": "Logged out successfully",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -21,7 +22,6 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
@@ -39,14 +39,15 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
// contains random token, redirect url, role
|
||||
sessionSplit := strings.Split(state, "___")
|
||||
|
||||
// TODO validate redirect url
|
||||
if len(sessionSplit) < 2 {
|
||||
if len(sessionSplit) < 3 {
|
||||
c.JSON(400, gin.H{"error": "invalid redirect url"})
|
||||
return
|
||||
}
|
||||
|
||||
inputRoles := strings.Split(sessionSplit[2], ",")
|
||||
stateValue := sessionSplit[0]
|
||||
redirectURL := sessionSplit[1]
|
||||
inputRoles := strings.Split(sessionSplit[2], ",")
|
||||
scopes := strings.Split(sessionSplit[3], ",")
|
||||
|
||||
var err error
|
||||
user := models.User{}
|
||||
@@ -70,6 +71,10 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
existingUser, err := db.Provider.GetUserByEmail(user.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
c.JSON(400, gin.H{"error": "signup is disabled for this instance"})
|
||||
return
|
||||
}
|
||||
// user not registered, register user and generate session token
|
||||
user.SignupMethods = provider
|
||||
// make sure inputRoles don't include protected roles
|
||||
@@ -145,17 +150,29 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use query param
|
||||
scope := []string{"openid", "email", "profile"}
|
||||
nonce := uuid.New().String()
|
||||
_, newSessionToken, err := token.CreateSessionToken(user, nonce, inputRoles, scope)
|
||||
authToken, err := token.CreateAuthToken(c, user, inputRoles, scopes)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
}
|
||||
expiresIn := int64(1800)
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + stateValue + "&id_token=" + authToken.IDToken.Token
|
||||
|
||||
cookie.SetSession(c, authToken.FingerPrintHash)
|
||||
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
params = params + `&refresh_token=` + authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
sessionstore.SetState(newSessionToken, nonce+"@"+user.ID)
|
||||
cookie.SetSession(c, newSessionToken)
|
||||
go utils.SaveSessionInDB(c, user.ID)
|
||||
if strings.Contains(redirectURL, "?") {
|
||||
redirectURL = redirectURL + "&" + params
|
||||
} else {
|
||||
redirectURL = redirectURL + "?" + params
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
}
|
||||
}
|
||||
|
@@ -10,23 +10,42 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
|
||||
func OAuthLoginHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
hostname := utils.GetHost(c)
|
||||
redirectURL := c.Query("redirectURL")
|
||||
roles := c.Query("roles")
|
||||
// deprecating redirectURL instead use redirect_uri
|
||||
redirectURI := strings.TrimSpace(c.Query("redirectURL"))
|
||||
if redirectURI == "" {
|
||||
redirectURI = strings.TrimSpace(c.Query("redirect_uri"))
|
||||
}
|
||||
roles := strings.TrimSpace(c.Query("roles"))
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||
|
||||
if redirectURL == "" {
|
||||
if redirectURI == "" {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid redirect url",
|
||||
"error": "invalid redirect uri",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if state == "" {
|
||||
c.JSON(400, gin.H{
|
||||
"error": "invalid state",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var scope []string
|
||||
if scopeString == "" {
|
||||
scope = []string{"openid", "profile", "email"}
|
||||
} else {
|
||||
scope = strings.Split(scopeString, " ")
|
||||
}
|
||||
|
||||
if roles != "" {
|
||||
// validate role
|
||||
rolesSplit := strings.Split(roles, ",")
|
||||
@@ -43,8 +62,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
|
||||
roles = strings.Join(envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||
}
|
||||
|
||||
uuid := uuid.New()
|
||||
oauthStateString := uuid.String() + "___" + redirectURL + "___" + roles
|
||||
oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, ",")
|
||||
|
||||
provider := c.Param("oauth_provider")
|
||||
isProviderConfigured := true
|
||||
|
50
server/handlers/revoke.go
Normal file
50
server/handlers/revoke.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Revoke handler to revoke refresh token
|
||||
func RevokeHandler() gin.HandlerFunc {
|
||||
return func(gc *gin.Context) {
|
||||
var reqBody map[string]string
|
||||
if err := gc.BindJSON(&reqBody); err != nil {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "error_binding_json",
|
||||
"error_description": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// get fingerprint hash
|
||||
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
|
||||
clientID := strings.TrimSpace(reqBody["client_id"])
|
||||
|
||||
if clientID == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "client_id_required",
|
||||
"error_description": "The client id is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if clientID != envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID) {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_client_id",
|
||||
"error_description": "The client id is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sessionstore.RemoveState(refreshToken)
|
||||
|
||||
gc.JSON(http.StatusOK, gin.H{
|
||||
"message": "Token revoked successfully",
|
||||
})
|
||||
}
|
||||
}
|
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// TokenHandler to handle /oauth/token requests
|
||||
// grant type required
|
||||
func TokenHandler() gin.HandlerFunc {
|
||||
return func(gc *gin.Context) {
|
||||
var reqBody map[string]string
|
||||
@@ -29,6 +31,22 @@ func TokenHandler() gin.HandlerFunc {
|
||||
codeVerifier := strings.TrimSpace(reqBody["code_verifier"])
|
||||
code := strings.TrimSpace(reqBody["code"])
|
||||
clientID := strings.TrimSpace(reqBody["client_id"])
|
||||
grantType := strings.TrimSpace(reqBody["grant_type"])
|
||||
refreshToken := strings.TrimSpace(reqBody["refresh_token"])
|
||||
|
||||
if grantType == "" {
|
||||
grantType = "authorization_code"
|
||||
}
|
||||
|
||||
isRefreshTokenGrant := grantType == "refresh_token"
|
||||
isAuthorizationCodeGrant := grantType == "authorization_code"
|
||||
|
||||
if !isRefreshTokenGrant && !isAuthorizationCodeGrant {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_grant_type",
|
||||
"error_description": "grant_type is invalid",
|
||||
})
|
||||
}
|
||||
|
||||
if clientID == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
@@ -46,58 +64,95 @@ func TokenHandler() gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
if codeVerifier == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is required",
|
||||
})
|
||||
return
|
||||
var userID string
|
||||
var roles, scope []string
|
||||
if isAuthorizationCodeGrant {
|
||||
|
||||
if codeVerifier == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code",
|
||||
"error_description": "The code is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(codeVerifier))
|
||||
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
|
||||
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
|
||||
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
|
||||
sessionData := sessionstore.GetState(encryptedCode)
|
||||
if sessionData == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// split session data
|
||||
// it contains code@sessiontoken
|
||||
sessionDataSplit := strings.Split(sessionData, "@")
|
||||
|
||||
if sessionDataSplit[0] != code {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// validate session
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
||||
if err != nil {
|
||||
gc.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"error_description": "Invalid session data",
|
||||
})
|
||||
return
|
||||
}
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
userID = claims.Subject
|
||||
roles = claims.Roles
|
||||
scope = claims.Scope
|
||||
} else {
|
||||
// validate refresh token
|
||||
if refreshToken == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_refresh_token",
|
||||
"error_description": "The refresh token is invalid",
|
||||
})
|
||||
}
|
||||
|
||||
claims, err := token.ValidateRefreshToken(gc, refreshToken)
|
||||
if err != nil {
|
||||
gc.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"error_description": err.Error(),
|
||||
})
|
||||
}
|
||||
userID = claims["sub"].(string)
|
||||
rolesInterface := claims["roles"].([]interface{})
|
||||
scopeInterface := claims["scope"].([]interface{})
|
||||
for _, v := range rolesInterface {
|
||||
roles = append(roles, v.(string))
|
||||
}
|
||||
for _, v := range scopeInterface {
|
||||
scope = append(scope, v.(string))
|
||||
}
|
||||
// remove older refresh token and rotate it for security
|
||||
sessionstore.RemoveState(refreshToken)
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code",
|
||||
"error_description": "The code is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(codeVerifier))
|
||||
encryptedCode := strings.ReplaceAll(base64.URLEncoding.EncodeToString(hash.Sum(nil)), "+", "-")
|
||||
encryptedCode = strings.ReplaceAll(encryptedCode, "/", "_")
|
||||
encryptedCode = strings.ReplaceAll(encryptedCode, "=", "")
|
||||
sessionData := sessionstore.GetState(encryptedCode)
|
||||
if sessionData == "" {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// split session data
|
||||
// it contains code@sessiontoken
|
||||
sessionDataSplit := strings.Split(sessionData, "@")
|
||||
|
||||
if sessionDataSplit[0] != code {
|
||||
gc.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid_code_verifier",
|
||||
"error_description": "The code verifier is invalid",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// validate session
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionDataSplit[1])
|
||||
if err != nil {
|
||||
gc.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"error_description": "Invalid session data",
|
||||
})
|
||||
return
|
||||
}
|
||||
userID := claims.Subject
|
||||
user, err := db.Provider.GetUserByID(userID)
|
||||
if err != nil {
|
||||
gc.JSON(http.StatusUnauthorized, gin.H{
|
||||
@@ -106,9 +161,8 @@ func TokenHandler() gin.HandlerFunc {
|
||||
})
|
||||
return
|
||||
}
|
||||
// rollover the session for security
|
||||
sessionstore.RemoveState(sessionDataSplit[1])
|
||||
authToken, err := token.CreateAuthToken(gc, user, claims.Roles, claims.Scope)
|
||||
|
||||
authToken, err := token.CreateAuthToken(gc, user, roles, scope)
|
||||
if err != nil {
|
||||
gc.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
@@ -124,13 +178,14 @@ func TokenHandler() gin.HandlerFunc {
|
||||
res := map[string]interface{}{
|
||||
"access_token": authToken.AccessToken.Token,
|
||||
"id_token": authToken.IDToken.Token,
|
||||
"scope": claims.Scope,
|
||||
"scope": scope,
|
||||
"roles": roles,
|
||||
"expires_in": expiresIn,
|
||||
}
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
res["refresh_token"] = authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
gc.JSON(http.StatusOK, res)
|
||||
|
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/authorizerdev/authorizer/server/token"
|
||||
"github.com/authorizerdev/authorizer/server/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// VerifyEmailHandler handles the verify email route.
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
func VerifyEmailHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
errorRes := gin.H{
|
||||
"error": "invalid token",
|
||||
"error": "invalid_token",
|
||||
}
|
||||
tokenInQuery := c.Query("token")
|
||||
if tokenInQuery == "" {
|
||||
@@ -29,30 +29,24 @@ func VerifyEmailHandler() gin.HandlerFunc {
|
||||
|
||||
verificationRequest, err := db.Provider.GetVerificationRequestByToken(tokenInQuery)
|
||||
if err != nil {
|
||||
errorRes["error_description"] = err.Error()
|
||||
c.JSON(400, errorRes)
|
||||
return
|
||||
}
|
||||
|
||||
// verify if token exists in db
|
||||
hostname := utils.GetHost(c)
|
||||
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
claim, err := token.ParseJWTToken(tokenInQuery, hostname, encryptedNonce, verificationRequest.Email)
|
||||
claim, err := token.ParseJWTToken(tokenInQuery, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
||||
if err != nil {
|
||||
errorRes["error_description"] = err.Error()
|
||||
c.JSON(400, errorRes)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := db.Provider.GetUserByEmail(claim["sub"].(string))
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
errorRes["error_description"] = err.Error()
|
||||
c.JSON(400, errorRes)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -65,21 +59,53 @@ func VerifyEmailHandler() gin.HandlerFunc {
|
||||
// delete from verification table
|
||||
db.Provider.DeleteVerificationRequest(verificationRequest)
|
||||
|
||||
roles := strings.Split(user.Roles, ",")
|
||||
scope := []string{"openid", "email", "profile"}
|
||||
nonce := uuid.New().String()
|
||||
_, authToken, err := token.CreateSessionToken(user, nonce, roles, scope)
|
||||
state := strings.TrimSpace(c.Query("state"))
|
||||
redirectURL := strings.TrimSpace(c.Query("redirect_uri"))
|
||||
rolesString := strings.TrimSpace(c.Query("roles"))
|
||||
var roles []string
|
||||
if rolesString == "" {
|
||||
roles = strings.Split(user.Roles, ",")
|
||||
} else {
|
||||
roles = strings.Split(rolesString, ",")
|
||||
}
|
||||
|
||||
scopeString := strings.TrimSpace(c.Query("scope"))
|
||||
var scope []string
|
||||
if scopeString == "" {
|
||||
scope = []string{"openid", "email", "profile"}
|
||||
} else {
|
||||
scope = strings.Split(scopeString, " ")
|
||||
}
|
||||
authToken, err := token.CreateAuthToken(c, user, roles, scope)
|
||||
if err != nil {
|
||||
c.JSON(400, gin.H{
|
||||
"message": err.Error(),
|
||||
})
|
||||
errorRes["error_description"] = err.Error()
|
||||
c.JSON(500, errorRes)
|
||||
return
|
||||
}
|
||||
sessionstore.SetState(authToken, nonce+"@"+user.ID)
|
||||
cookie.SetSession(c, authToken)
|
||||
expiresIn := int64(1800)
|
||||
params := "access_token=" + authToken.AccessToken.Token + "&token_type=bearer&expires_in=" + strconv.FormatInt(expiresIn, 10) + "&state=" + state + "&id_token=" + authToken.IDToken.Token
|
||||
|
||||
cookie.SetSession(c, authToken.FingerPrintHash)
|
||||
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
params = params + `&refresh_token=${refresh_token}`
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
if redirectURL == "" {
|
||||
redirectURL = claim["redirect_uri"].(string)
|
||||
}
|
||||
|
||||
if strings.Contains(redirectURL, "?") {
|
||||
redirectURL = redirectURL + "&" + params
|
||||
} else {
|
||||
redirectURL = redirectURL + "?" + params
|
||||
}
|
||||
|
||||
go utils.SaveSessionInDB(c, user.ID)
|
||||
|
||||
c.Redirect(http.StatusTemporaryRedirect, claim["redirect_url"].(string))
|
||||
c.Redirect(http.StatusTemporaryRedirect, redirectURL)
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,8 @@ func main() {
|
||||
envstore.ARG_ENV_FILE = flag.String("env_file", "", "Env file path")
|
||||
flag.Parse()
|
||||
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, VERSION)
|
||||
log.Println("=> version:", VERSION)
|
||||
constants.VERSION = VERSION
|
||||
|
||||
// initialize required envs (mainly db & env file path)
|
||||
err := env.InitRequiredEnv()
|
||||
|
@@ -53,6 +53,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
disableBasicAuthentication := store.BoolEnv[constants.EnvKeyDisableBasicAuthentication]
|
||||
disableMagicLinkLogin := store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin]
|
||||
disableLoginPage := store.BoolEnv[constants.EnvKeyDisableLoginPage]
|
||||
disableSignUp := store.BoolEnv[constants.EnvKeyDisableSignUp]
|
||||
roles := store.SliceEnv[constants.EnvKeyRoles]
|
||||
defaultRoles := store.SliceEnv[constants.EnvKeyDefaultRoles]
|
||||
protectedRoles := store.SliceEnv[constants.EnvKeyProtectedRoles]
|
||||
@@ -92,6 +93,7 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
||||
DisableBasicAuthentication: &disableBasicAuthentication,
|
||||
DisableMagicLinkLogin: &disableMagicLinkLogin,
|
||||
DisableLoginPage: &disableLoginPage,
|
||||
DisableSignUp: &disableSignUp,
|
||||
Roles: roles,
|
||||
ProtectedRoles: protectedRoles,
|
||||
DefaultRoles: defaultRoles,
|
||||
|
@@ -39,20 +39,26 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
||||
}
|
||||
|
||||
hostname := utils.GetHost(gc)
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash)
|
||||
redirectURL := utils.GetAppURL(gc) + "/reset-password"
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: constants.VerificationTypeForgotPassword,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonce,
|
||||
Token: verificationToken,
|
||||
Identifier: constants.VerificationTypeForgotPassword,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
})
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
|
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
|
||||
}
|
@@ -69,8 +69,6 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
return res, err
|
||||
}
|
||||
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
|
||||
expiresIn := int64(1800)
|
||||
res = &model.AuthResponse{
|
||||
Message: `Logged in successfully`,
|
||||
@@ -80,12 +78,13 @@ func LoginResolver(ctx context.Context, params model.LoginInput) (*model.AuthRes
|
||||
User: user.AsAPIUser(),
|
||||
}
|
||||
|
||||
cookie.SetSession(gc, authToken.FingerPrintHash)
|
||||
sessionstore.SetState(authToken.FingerPrintHash, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
res.RefreshToken = &authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
go utils.SaveSessionInDB(gc, user.ID)
|
||||
|
@@ -43,8 +43,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
|
||||
// find user with email
|
||||
existingUser, err := db.Provider.GetUserByEmail(params.Email)
|
||||
|
||||
if err != nil {
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
user.SignupMethods = constants.SignupMethodMagicLinkLogin
|
||||
// define roles for new user
|
||||
if len(params.Roles) > 0 {
|
||||
@@ -68,6 +71,9 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
// Need to modify roles in this case
|
||||
|
||||
// find the unassigned roles
|
||||
if len(params.Roles) <= 0 {
|
||||
inputRoles = envstore.EnvStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles)
|
||||
}
|
||||
existingRoles := strings.Split(existingUser.Roles, ",")
|
||||
unasignedRoles := []string{}
|
||||
for _, ir := range inputRoles {
|
||||
@@ -109,24 +115,46 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
||||
hostname := utils.GetHost(gc)
|
||||
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||
// insert verification request
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
redirectURLParams := "&roles=" + strings.Join(inputRoles, ",")
|
||||
if params.State != nil {
|
||||
redirectURLParams = redirectURLParams + "&state=" + *params.State
|
||||
}
|
||||
if params.Scope != nil && len(params.Scope) > 0 {
|
||||
redirectURLParams = redirectURLParams + "&scope=" + strings.Join(params.Scope, " ")
|
||||
}
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
|
||||
if strings.Contains(redirectURL, "?") {
|
||||
redirectURL = redirectURL + "&" + redirectURLParams
|
||||
} else {
|
||||
redirectURL = redirectURL + "?" + redirectURLParams
|
||||
}
|
||||
|
||||
verificationType := constants.VerificationTypeMagicLinkLogin
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash)
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonce,
|
||||
_, err = db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
})
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
// exec it as go routing so that we can reduce the api latency
|
||||
go email.SendVerificationMail(params.Email, verificationToken, hostname)
|
||||
}
|
||||
|
||||
|
@@ -44,20 +44,22 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
|
||||
}
|
||||
|
||||
hostname := utils.GetHost(gc)
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash)
|
||||
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname, nonceHash, verificationRequest.RedirectURI)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: params.Identifier,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonce,
|
||||
Token: verificationToken,
|
||||
Identifier: params.Identifier,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: verificationRequest.RedirectURI,
|
||||
})
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
|
@@ -37,11 +37,7 @@ func ResetPasswordResolver(ctx context.Context, params model.ResetPasswordInput)
|
||||
|
||||
// verify if token exists in db
|
||||
hostname := utils.GetHost(gc)
|
||||
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf(`invalid token`)
|
||||
}
|
||||
|
16
server/resolvers/revoke.go
Normal file
16
server/resolvers/revoke.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||
)
|
||||
|
||||
// RevokeResolver resolver to revoke refresh token
|
||||
func RevokeResolver(ctx context.Context, params model.OAuthRevokeInput) (*model.Response, error) {
|
||||
sessionstore.RemoveState(params.RefreshToken)
|
||||
return &model.Response{
|
||||
Message: "Token revoked",
|
||||
}, nil
|
||||
}
|
@@ -2,7 +2,9 @@ package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/cookie"
|
||||
"github.com/authorizerdev/authorizer/server/db"
|
||||
@@ -24,13 +26,15 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
|
||||
|
||||
sessionToken, err := cookie.GetSession(gc)
|
||||
if err != nil {
|
||||
return res, err
|
||||
log.Println("error getting session token:", err)
|
||||
return res, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
// get session from cookie
|
||||
claims, err := token.ValidateBrowserSession(gc, sessionToken)
|
||||
if err != nil {
|
||||
return res, err
|
||||
log.Println("session validation failed:", err)
|
||||
return res, errors.New("unauthorized")
|
||||
}
|
||||
userID := claims.Subject
|
||||
user, err := db.Provider.GetUserByID(userID)
|
||||
@@ -80,7 +84,7 @@ func SessionResolver(ctx context.Context, params *model.SessionQueryInput) (*mod
|
||||
|
||||
if authToken.RefreshToken != nil {
|
||||
res.RefreshToken = &authToken.RefreshToken.Token
|
||||
sessionstore.SetState(authToken.AccessToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
sessionstore.SetState(authToken.RefreshToken.Token, authToken.FingerPrint+"@"+user.ID)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
@@ -28,9 +28,14 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
return res, err
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp) {
|
||||
return res, fmt.Errorf(`signup is disabled for this instance`)
|
||||
}
|
||||
|
||||
if envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
||||
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
||||
}
|
||||
|
||||
if params.ConfirmPassword != params.Password {
|
||||
return res, fmt.Errorf(`password and confirm password does not match`)
|
||||
}
|
||||
@@ -123,21 +128,26 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
hostname := utils.GetHost(gc)
|
||||
if !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||
// insert verification request
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeBasicAuthSignup
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
if params.RedirectURI != nil {
|
||||
redirectURL = *params.RedirectURI
|
||||
}
|
||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonce,
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: params.Email,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
})
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
@@ -149,6 +159,9 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
||||
}
|
||||
} else {
|
||||
scope := []string{"openid", "email", "profile"}
|
||||
if params.Scope != nil && len(scope) > 0 {
|
||||
scope = params.Scope
|
||||
}
|
||||
|
||||
authToken, err := token.CreateAuthToken(gc, user, roles, scope)
|
||||
if err != nil {
|
||||
|
@@ -129,21 +129,23 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
||||
user.EmailVerifiedAt = nil
|
||||
hasEmailChanged = true
|
||||
// insert verification request
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeUpdateEmail
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: newEmail,
|
||||
Nonce: nonce,
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: newEmail,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
})
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
|
@@ -101,21 +101,23 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
user.Email = newEmail
|
||||
user.EmailVerifiedAt = nil
|
||||
// insert verification request
|
||||
nonce, nonceHash, err := utils.GenerateNonce()
|
||||
_, nonceHash, err := utils.GenerateNonce()
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
verificationType := constants.VerificationTypeUpdateEmail
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash)
|
||||
redirectURL := utils.GetAppURL(gc)
|
||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname, nonceHash, redirectURL)
|
||||
if err != nil {
|
||||
log.Println(`error generating token`, err)
|
||||
}
|
||||
db.Provider.AddVerificationRequest(models.VerificationRequest{
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: newEmail,
|
||||
Nonce: nonce,
|
||||
Token: verificationToken,
|
||||
Identifier: verificationType,
|
||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||
Email: newEmail,
|
||||
Nonce: nonceHash,
|
||||
RedirectURI: redirectURL,
|
||||
})
|
||||
|
||||
// exec it as go routin so that we can reduce the api latency
|
||||
@@ -152,6 +154,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
return res, err
|
||||
}
|
||||
|
||||
createdAt := user.CreatedAt
|
||||
updatedAt := user.UpdatedAt
|
||||
res = &model.User{
|
||||
ID: params.ID,
|
||||
Email: user.Email,
|
||||
@@ -159,8 +163,8 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
||||
GivenName: user.GivenName,
|
||||
FamilyName: user.FamilyName,
|
||||
Roles: strings.Split(user.Roles, ","),
|
||||
CreatedAt: &user.CreatedAt,
|
||||
UpdatedAt: &user.UpdatedAt,
|
||||
CreatedAt: &createdAt,
|
||||
UpdatedAt: &updatedAt,
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
@@ -29,11 +29,7 @@ func VerifyEmailResolver(ctx context.Context, params model.VerifyEmailInput) (*m
|
||||
|
||||
// verify if token exists in db
|
||||
hostname := utils.GetHost(gc)
|
||||
encryptedNonce, err := utils.EncryptNonce(verificationRequest.Nonce)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, encryptedNonce, verificationRequest.Email)
|
||||
claim, err := token.ParseJWTToken(params.Token, hostname, verificationRequest.Nonce, verificationRequest.Email)
|
||||
if err != nil {
|
||||
return res, fmt.Errorf(`invalid token: %s`, err.Error())
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ func InitRouter() *gin.Engine {
|
||||
router.GET("/userinfo", handlers.UserInfoHandler())
|
||||
router.GET("/logout", handlers.LogoutHandler())
|
||||
router.POST("/oauth/token", handlers.TokenHandler())
|
||||
router.POST("/oauth/revoke", handlers.RevokeHandler())
|
||||
|
||||
router.LoadHTMLGlob("templates/*")
|
||||
// login page app related routes.
|
||||
@@ -43,6 +44,7 @@ func InitRouter() *gin.Engine {
|
||||
{
|
||||
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
||||
dashboard.Static("/build", "dashboard/build")
|
||||
dashboard.Static("/public", "dashboard/public")
|
||||
dashboard.GET("/", handlers.DashboardHandler())
|
||||
dashboard.GET("/:page", handlers.DashboardHandler())
|
||||
}
|
||||
|
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])
|
||||
})
|
||||
}
|
@@ -15,7 +15,7 @@ func TestResolvers(t *testing.T) {
|
||||
// constants.DbTypeArangodb: "http://localhost:8529",
|
||||
// constants.DbTypeMongodb: "mongodb://localhost:27017",
|
||||
}
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, "test")
|
||||
|
||||
for dbType, dbURL := range databases {
|
||||
s := testSetup()
|
||||
envstore.EnvStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
|
||||
@@ -62,6 +62,7 @@ func TestResolvers(t *testing.T) {
|
||||
magicLinkLoginTests(t, s)
|
||||
logoutTests(t, s)
|
||||
metaTests(t, s)
|
||||
inviteUserTest(t, s)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
|
||||
}
|
||||
|
||||
if utils.StringSliceContains(scope, "offline_access") {
|
||||
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, hostname, nonce)
|
||||
refreshToken, refreshTokenExpiresAt, err := CreateRefreshToken(user, roles, scope, hostname, nonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func CreateAuthToken(gc *gin.Context, user models.User, roles, scope []string) (
|
||||
}
|
||||
|
||||
// CreateRefreshToken util to create JWT token
|
||||
func CreateRefreshToken(user models.User, roles []string, hostname, nonce string) (string, int64, error) {
|
||||
func CreateRefreshToken(user models.User, roles, scopes []string, hostname, nonce string) (string, int64, error) {
|
||||
// expires in 1 year
|
||||
expiryBound := time.Hour * 8760
|
||||
expiresAt := time.Now().Add(expiryBound).Unix()
|
||||
@@ -115,6 +115,7 @@ func CreateRefreshToken(user models.User, roles []string, hostname, nonce string
|
||||
"iat": time.Now().Unix(),
|
||||
"token_type": constants.TokenTypeRefreshToken,
|
||||
"roles": roles,
|
||||
"scope": scopes,
|
||||
"nonce": nonce,
|
||||
}
|
||||
|
||||
@@ -198,6 +199,36 @@ func ValidateAccessToken(gc *gin.Context, accessToken string) (map[string]interf
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Function to validate refreshToken
|
||||
func ValidateRefreshToken(gc *gin.Context, refreshToken string) (map[string]interface{}, error) {
|
||||
var res map[string]interface{}
|
||||
|
||||
if refreshToken == "" {
|
||||
return res, fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
savedSession := sessionstore.GetState(refreshToken)
|
||||
if savedSession == "" {
|
||||
return res, fmt.Errorf(`unauthorized`)
|
||||
}
|
||||
|
||||
savedSessionSplit := strings.Split(savedSession, "@")
|
||||
nonce := savedSessionSplit[0]
|
||||
userID := savedSessionSplit[1]
|
||||
|
||||
hostname := utils.GetHost(gc)
|
||||
res, err := ParseJWTToken(refreshToken, hostname, nonce, userID)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if res["token_type"] != constants.TokenTypeRefreshToken {
|
||||
return res, fmt.Errorf(`unauthorized: invalid token type`)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ValidateBrowserSession(gc *gin.Context, encryptedSession string) (*SessionData, error) {
|
||||
if encryptedSession == "" {
|
||||
return nil, fmt.Errorf(`unauthorized`)
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// CreateVerificationToken creates a verification JWT token
|
||||
func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (string, error) {
|
||||
func CreateVerificationToken(email, tokenType, hostname, nonceHash, redirectURL string) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"iss": hostname,
|
||||
"aud": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
|
||||
@@ -18,7 +18,7 @@ func CreateVerificationToken(email, tokenType, hostname, nonceHash string) (stri
|
||||
"iat": time.Now().Unix(),
|
||||
"token_type": tokenType,
|
||||
"nonce": nonceHash,
|
||||
"redirect_url": envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL),
|
||||
"redirect_uri": redirectURL,
|
||||
}
|
||||
|
||||
return SignJWTToken(claims)
|
||||
|
@@ -9,7 +9,7 @@ import (
|
||||
// GetMeta helps in getting the meta data about the deployment from EnvData
|
||||
func GetMetaInfo() model.Meta {
|
||||
return model.Meta{
|
||||
Version: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyVersion),
|
||||
Version: constants.VERSION,
|
||||
ClientID: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyClientID),
|
||||
IsGoogleLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret) != "",
|
||||
IsGithubLoginEnabled: envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID) != "" && envstore.EnvStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret) != "",
|
||||
@@ -17,5 +17,6 @@ func GetMetaInfo() model.Meta {
|
||||
IsBasicAuthenticationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication),
|
||||
IsEmailVerificationEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification),
|
||||
IsMagicLinkLoginEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin),
|
||||
IsSignUpEnabled: !envstore.EnvStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableSignUp),
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/authorizerdev/authorizer/server/constants"
|
||||
"github.com/authorizerdev/authorizer/server/envstore"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -71,3 +73,12 @@ func GetDomainName(uri string) string {
|
||||
|
||||
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
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>{{.data.organizationName}}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/app/favicon_io/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/app/favicon_io/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/app/favicon_io/favicon-16x16.png">
|
||||
|
@@ -8,7 +8,6 @@
|
||||
(function (window, document) {
|
||||
var targetOrigin = {{.target_origin}};
|
||||
var authorizationResponse = {{.authorization_response}};
|
||||
console.log({targetOrigin})
|
||||
window.parent.postMessage(authorizationResponse, targetOrigin);
|
||||
})(this, this.document);
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user