Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1f81e45e79 | ||
![]() |
40dcf67de9 | ||
![]() |
32d8b7c038 | ||
![]() |
2a91f3e7d8 | ||
![]() |
36d9861517 | ||
![]() |
d577a21a9a | ||
![]() |
115607cb6b | ||
![]() |
adb969ec04 | ||
![]() |
4e48320cf1 | ||
![]() |
cfe035e96b | ||
![]() |
34a91f3195 | ||
![]() |
ea14cc1743 | ||
![]() |
9d7f5fd9db | ||
![]() |
0520056e43 | ||
![]() |
1821e27692 | ||
![]() |
388530a69c | ||
![]() |
1e32d790b3 | ||
![]() |
681ffc65f1 | ||
![]() |
6331ec7b7a | ||
![]() |
25c9ce03bd | ||
![]() |
ac416bfc7b | ||
![]() |
0049e1380b | ||
![]() |
9bd185a9c6 | ||
![]() |
82bc38923c | ||
![]() |
8df8010b22 |
14
.env.sample
@@ -1,16 +1,2 @@
|
|||||||
ENV=production
|
|
||||||
DATABASE_URL=data.db
|
DATABASE_URL=data.db
|
||||||
DATABASE_TYPE=sqlite
|
DATABASE_TYPE=sqlite
|
||||||
ADMIN_SECRET=admin
|
|
||||||
JWT_SECRET=random_string
|
|
||||||
SENDER_EMAIL=info@authorizer.dev
|
|
||||||
SMTP_USERNAME=username
|
|
||||||
SMTP_PASSWORD=password
|
|
||||||
SMTP_HOST=smtp.mailtrap.io
|
|
||||||
SMTP_PORT=2525
|
|
||||||
JWT_TYPE=HS256
|
|
||||||
ROLES=user
|
|
||||||
DEFAULT_ROLES=user
|
|
||||||
PROTECTED_ROLES=admin
|
|
||||||
JWT_ROLE_CLAIM=role
|
|
||||||
CUSTOM_ACCESS_TOKEN_SCRIPT=function(user,tokenPayload){var data = tokenPayload;data.extra = {'x-extra-id': user.id};return data;}
|
|
@@ -24,7 +24,9 @@ FROM alpine:latest
|
|||||||
WORKDIR /root/
|
WORKDIR /root/
|
||||||
RUN mkdir app dashboard
|
RUN mkdir app dashboard
|
||||||
COPY --from=node-builder /authorizer/app/build app/build
|
COPY --from=node-builder /authorizer/app/build app/build
|
||||||
|
COPY --from=node-builder /authorizer/app/favicon_io app/favicon_io
|
||||||
COPY --from=node-builder /authorizer/dashboard/build dashboard/build
|
COPY --from=node-builder /authorizer/dashboard/build dashboard/build
|
||||||
|
COPY --from=node-builder /authorizer/dashboard/favicon_io dashboard/favicon_io
|
||||||
COPY --from=go-builder /authorizer/build build
|
COPY --from=go-builder /authorizer/build build
|
||||||
COPY templates templates
|
COPY templates templates
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
19
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://authorizer.dev">
|
<a href="https://authorizer.dev">
|
||||||
<img alt="Logo" src="https://github.com/authorizerdev/authorizer/blob/main/assets/logo.png" width="60" />
|
<img alt="Logo" src="https://authorizer.dev/images/logo.png" width="60" />
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
@@ -129,18 +129,13 @@ Required environment variables are pre-configured in `.env` file. But based on t
|
|||||||
|
|
||||||
> Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server`
|
> Note: For mac users, you might have to give binary the permission to execute. Here is the command you can use to grant permission `xattr -d com.apple.quarantine build/server`
|
||||||
|
|
||||||
## Install instance on Heroku
|
Deploy production ready Authorizer instance using one click deployment options available below
|
||||||
|
|
||||||
Deploy Authorizer using [heroku](https://github.com/authorizerdev/authorizer-heroku) and quickly play with it in 30seconds
|
| **Infra provider** | **One-click link** | **Additional information** |
|
||||||
<br/><br/>
|
| :----------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------: |
|
||||||
[](https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku)
|
| Railway.app | <a href="https://railway.app/new/template?template=https://github.com/authorizerdev/authorizer-railway&plugins=postgresql,redis"><img src="https://railway.app/button.svg" style="height: 44px" alt="Deploy on Railway"></a> | [docs](https://docs.authorizer.dev/deployment/railway) |
|
||||||
|
| Heroku | <a href="https://heroku.com/deploy?template=https://github.com/authorizerdev/authorizer-heroku"><img src="https://www.herokucdn.com/deploy/button.svg" alt="Deploy to Heroku" style="height: 44px;"></a> | [docs](https://docs.authorizer.dev/deployment/heroku) |
|
||||||
# Install instance on railway
|
| Render | [](https://render.com/deploy?repo=https://github.com/authorizerdev/authorizer-render) | [docs](https://docs.authorizer.dev/deployment/render) |
|
||||||
|
|
||||||
Deploy production ready Authorizer instance using [railway.app](https://github.com/authorizerdev/authorizer-railway) with postgres and redis for free and build with it in 30seconds
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
[](https://railway.app/new/template?template=https%3A%2F%2Fgithub.com%2Fauthorizerdev%2Fauthorizer-railway&plugins=postgresql%2Credis&envs=ENV%2CDATABASE_TYPE%2CADMIN_SECRET%2CCOOKIE_NAME%2CJWT_ROLE_CLAIM%2CJWT_TYPE%2CJWT_SECRET%2CFACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&optionalEnvs=FACEBOOK_CLIENT_ID%2CFACEBOOK_CLIENT_SECRET%2CGOOGLE_CLIENT_ID%2CGOOGLE_CLIENT_SECRET%2CGITHUB_CLIENT_ID%2CGITHUB_CLIENT_SECRET%2CALLOWED_ORIGINS%2CROLES%2CPROTECTED_ROLES%2CDEFAULT_ROLES&ENVDesc=Deployment+environment&DATABASE_TYPEDesc=With+railway+we+are+deploying+postgres+db&ADMIN_SECRETDesc=Secret+to+access+the+admin+apis&COOKIE_NAMEDesc=Name+of+http+only+cookie+that+will+be+used+as+session&FACEBOOK_CLIENT_IDDesc=Facebook+client+ID+for+facebook+login&FACEBOOK_CLIENT_SECRETDesc=Facebook+client+secret+for+facebook+login&GOOGLE_CLIENT_IDDesc=Google+client+ID+for+google+login&GOOGLE_CLIENT_SECRETDesc=Google+client+secret+for+google+login&GITHUB_CLIENT_IDDesc=Github+client+ID+for+github+login&GITHUB_CLIENT_SECRETDesc=Github+client+secret+for+github+login&ALLOWED_ORIGINSDesc=Whitelist+the+URL+for+which+this+instance+of+authorizer+is+allowed&ROLESDesc=Comma+separated+list+of+roles+that+platform+supports.+Default+role+is+user&PROTECTED_ROLESDesc=Comma+separated+list+of+protected+roles+for+which+sign-up+is+disabled&DEFAULT_ROLESDesc=Default+role+that+should+be+assigned+to+user.+It+should+be+one+from+the+list+of+%60ROLES%60+env.+Default+role+is+user&JWT_ROLE_CLAIMDesc=JWT+key+to+be+used+to+validate+the+role+field.&JWT_TYPEDesc=JWT+encryption+type&JWT_SECRETDesc=Random+string+that+will+be+used+for+encrypting+the+JWT+token&ENVDefault=PRODUCTION&DATABASE_TYPEDefault=postgres&COOKIE_NAMEDefault=authorizer&JWT_TYPEDefault=HS256&JWT_ROLE_CLAIMDefault=role)
|
|
||||||
|
|
||||||
### Things to consider
|
### Things to consider
|
||||||
|
|
||||||
|
BIN
app/favicon_io/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
app/favicon_io/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
app/favicon_io/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
app/favicon_io/favicon-16x16.png
Normal file
After Width: | Height: | Size: 528 B |
BIN
app/favicon_io/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
app/favicon_io/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
@@ -15,7 +15,7 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>Hey 👋,</h1>
|
<h1>Hey 👋,</h1>
|
||||||
<p>Thank you for joining authorizer demo app.</p>
|
<p>Thank you for using authorizer.</p>
|
||||||
<p>
|
<p>
|
||||||
Your email address is{' '}
|
Your email address is{' '}
|
||||||
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>
|
<a href={`mailto:${user?.email}`} style={{ color: '#3B82F6' }}>
|
||||||
|
BIN
dashboard/favicon_io/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
dashboard/favicon_io/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
dashboard/favicon_io/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
dashboard/favicon_io/favicon-16x16.png
Normal file
After Width: | Height: | Size: 528 B |
BIN
dashboard/favicon_io/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dashboard/favicon_io/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
2320
dashboard/package-lock.json
generated
@@ -17,9 +17,11 @@
|
|||||||
"@types/react": "^17.0.38",
|
"@types/react": "^17.0.38",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-dom": "^5.3.2",
|
"@types/react-router-dom": "^5.3.2",
|
||||||
|
"dayjs": "^1.10.7",
|
||||||
"esbuild": "^0.14.9",
|
"esbuild": "^0.14.9",
|
||||||
"framer-motion": "^5.5.5",
|
"framer-motion": "^5.5.5",
|
||||||
"graphql": "^16.2.0",
|
"graphql": "^16.2.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
|
@@ -12,6 +12,7 @@ const queryClient = createClient({
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
requestPolicy: 'network-only',
|
||||||
});
|
});
|
||||||
|
|
||||||
const theme = extendTheme({
|
const theme = extendTheme({
|
||||||
|
112
dashboard/src/components/DeleteUserModal.tsx
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import { FaRegTrashAlt } from 'react-icons/fa';
|
||||||
|
import { DeleteUser } from '../graphql/mutation';
|
||||||
|
import { capitalizeFirstLetter } from '../utils';
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DeleteUserModal = ({
|
||||||
|
user,
|
||||||
|
updateUserList,
|
||||||
|
}: {
|
||||||
|
user: userDataTypes;
|
||||||
|
updateUserList: Function;
|
||||||
|
}) => {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [userData, setUserData] = React.useState<userDataTypes>({
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
React.useEffect(() => {
|
||||||
|
setUserData(user);
|
||||||
|
}, []);
|
||||||
|
const deleteHandler = async () => {
|
||||||
|
const res = await client
|
||||||
|
.mutation(DeleteUser, { params: { email: userData.email } })
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.error.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (res.data?._delete_user) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.data?._delete_user.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>Delete User</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Delete User</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Text fontSize="md">Are you sure?</Text>
|
||||||
|
<Flex
|
||||||
|
padding="5%"
|
||||||
|
marginTop="5%"
|
||||||
|
marginBottom="2%"
|
||||||
|
border="1px solid #ff7875"
|
||||||
|
borderRadius="5px"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm">
|
||||||
|
User <b>{user.email}</b> will be deleted permanently!
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaRegTrashAlt />}
|
||||||
|
colorScheme="red"
|
||||||
|
variant="solid"
|
||||||
|
onClick={deleteHandler}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Delete
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteUserModal;
|
250
dashboard/src/components/EditUserModal.tsx
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
Stack,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import { FaSave } from 'react-icons/fa';
|
||||||
|
import InputField from './InputField';
|
||||||
|
import {
|
||||||
|
ArrayInputType,
|
||||||
|
DateInputType,
|
||||||
|
SelectInputType,
|
||||||
|
TextInputType,
|
||||||
|
} from '../constants';
|
||||||
|
import { getObjectDiff } from '../utils';
|
||||||
|
import { UpdateUser } from '../graphql/mutation';
|
||||||
|
|
||||||
|
const GenderTypes = {
|
||||||
|
Undisclosed: null,
|
||||||
|
Male: 'Male',
|
||||||
|
Female: 'Female',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
given_name: string;
|
||||||
|
family_name: string;
|
||||||
|
middle_name: string;
|
||||||
|
nickname: string;
|
||||||
|
gender: string;
|
||||||
|
birthdate: string;
|
||||||
|
phone_number: string;
|
||||||
|
picture: string;
|
||||||
|
roles: [string] | [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditUserModal = ({
|
||||||
|
user,
|
||||||
|
updateUserList,
|
||||||
|
}: {
|
||||||
|
user: userDataTypes;
|
||||||
|
updateUserList: Function;
|
||||||
|
}) => {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
const [userData, setUserData] = React.useState<userDataTypes>({
|
||||||
|
id: '',
|
||||||
|
email: '',
|
||||||
|
given_name: '',
|
||||||
|
family_name: '',
|
||||||
|
middle_name: '',
|
||||||
|
nickname: '',
|
||||||
|
gender: '',
|
||||||
|
birthdate: '',
|
||||||
|
phone_number: '',
|
||||||
|
picture: '',
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
React.useEffect(() => {
|
||||||
|
setUserData(user);
|
||||||
|
}, []);
|
||||||
|
const saveHandler = async () => {
|
||||||
|
const diff = getObjectDiff(user, userData);
|
||||||
|
const updatedUserData = diff.reduce(
|
||||||
|
(acc: any, property: string) => ({
|
||||||
|
...acc,
|
||||||
|
// @ts-ignore
|
||||||
|
[property]: userData[property],
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateUser, { params: { ...updatedUserData, id: userData.id } })
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: 'User data update failed',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
} else if (res.data?._update_user?.id) {
|
||||||
|
toast({
|
||||||
|
title: 'User data update successful',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={onOpen}>Edit User Details</MenuItem>
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader>Edit User Details</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<Stack>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Given Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.GIVEN_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Middle Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.MIDDLE_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Family Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.FAMILY_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Birth Date:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={DateInputType.BIRTHDATE}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Nickname:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.NICKNAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Gender:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={SelectInputType.GENDER}
|
||||||
|
value={userData.gender}
|
||||||
|
options={GenderTypes}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Phone Number:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.PHONE_NUMBER}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Picture:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={TextInputType.PICTURE}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={userData}
|
||||||
|
setVariables={setUserData}
|
||||||
|
inputType={ArrayInputType.USER_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaSave />}
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={saveHandler}
|
||||||
|
isDisabled={false}
|
||||||
|
>
|
||||||
|
<Center h="100%" pt="5%">
|
||||||
|
Save
|
||||||
|
</Center>
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditUserModal;
|
338
dashboard/src/components/InputField.tsx
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
Center,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagRightIcon,
|
||||||
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Switch,
|
||||||
|
Code,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FaRegClone,
|
||||||
|
FaRegEye,
|
||||||
|
FaRegEyeSlash,
|
||||||
|
FaPlus,
|
||||||
|
FaTimes,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import {
|
||||||
|
ArrayInputOperations,
|
||||||
|
ArrayInputType,
|
||||||
|
SelectInputType,
|
||||||
|
HiddenInputType,
|
||||||
|
TextInputType,
|
||||||
|
TextAreaInputType,
|
||||||
|
SwitchInputType,
|
||||||
|
DateInputType,
|
||||||
|
} from '../constants';
|
||||||
|
import { copyTextToClipboard } from '../utils';
|
||||||
|
|
||||||
|
const InputField = ({
|
||||||
|
inputType,
|
||||||
|
variables,
|
||||||
|
setVariables,
|
||||||
|
fieldVisibility,
|
||||||
|
setFieldVisibility,
|
||||||
|
...downshiftProps
|
||||||
|
}: any) => {
|
||||||
|
const props = {
|
||||||
|
size: 'sm',
|
||||||
|
...downshiftProps,
|
||||||
|
};
|
||||||
|
const [inputFieldVisibility, setInputFieldVisibility] = React.useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({
|
||||||
|
ROLES: false,
|
||||||
|
DEFAULT_ROLES: false,
|
||||||
|
PROTECTED_ROLES: false,
|
||||||
|
ALLOWED_ORIGINS: false,
|
||||||
|
roles: false,
|
||||||
|
});
|
||||||
|
const [inputData, setInputData] = React.useState<Record<string, string>>({
|
||||||
|
ROLES: '',
|
||||||
|
DEFAULT_ROLES: '',
|
||||||
|
PROTECTED_ROLES: '',
|
||||||
|
ALLOWED_ORIGINS: '',
|
||||||
|
roles: '',
|
||||||
|
});
|
||||||
|
const updateInputHandler = (
|
||||||
|
type: string,
|
||||||
|
operation: any,
|
||||||
|
role: string = ''
|
||||||
|
) => {
|
||||||
|
if (operation === ArrayInputOperations.APPEND) {
|
||||||
|
if (inputData[type] !== '') {
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[type]: [...variables[type], inputData[type]],
|
||||||
|
});
|
||||||
|
setInputData({ ...inputData, [type]: '' });
|
||||||
|
}
|
||||||
|
setInputFieldVisibility({ ...inputFieldVisibility, [type]: false });
|
||||||
|
}
|
||||||
|
if (operation === ArrayInputOperations.REMOVE) {
|
||||||
|
let updatedEnvVars = variables[type].filter(
|
||||||
|
(item: string) => item !== role
|
||||||
|
);
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[type]: updatedEnvVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (Object.values(TextInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(
|
||||||
|
event: Event & {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
children={<FaRegClone color="#bfbfbf" />}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => copyTextToClipboard(variables[inputType])}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(HiddenInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={variables[inputType]}
|
||||||
|
onChange={(
|
||||||
|
event: Event & {
|
||||||
|
target: HTMLInputElement;
|
||||||
|
}
|
||||||
|
) =>
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type={!fieldVisibility[inputType] ? 'password' : 'text'}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
right="15px"
|
||||||
|
children={
|
||||||
|
<Flex>
|
||||||
|
{fieldVisibility[inputType] ? (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[inputType]: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEyeSlash color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[inputType]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEye color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => copyTextToClipboard(variables[inputType])}
|
||||||
|
>
|
||||||
|
<FaRegClone color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(ArrayInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
w="100%"
|
||||||
|
paddingTop="0.5%"
|
||||||
|
overflowX="scroll"
|
||||||
|
overflowY="hidden"
|
||||||
|
justifyContent="start"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
{variables[inputType].map((role: string, index: number) => (
|
||||||
|
<Box key={index} margin="0.5%" role="group">
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="gray"
|
||||||
|
minW="fit-content"
|
||||||
|
>
|
||||||
|
<TagLabel cursor="default">{role}</TagLabel>
|
||||||
|
<TagRightIcon
|
||||||
|
boxSize="12px"
|
||||||
|
as={FaTimes}
|
||||||
|
display="none"
|
||||||
|
cursor="pointer"
|
||||||
|
_groupHover={{ display: 'block' }}
|
||||||
|
onClick={() =>
|
||||||
|
updateInputHandler(
|
||||||
|
inputType,
|
||||||
|
ArrayInputOperations.REMOVE,
|
||||||
|
role
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
{inputFieldVisibility[inputType] ? (
|
||||||
|
<Box ml="1%" mb="0.75%">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
size="xs"
|
||||||
|
minW="150px"
|
||||||
|
placeholder="add a new value"
|
||||||
|
value={inputData[inputType]}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setInputData({ ...inputData, [inputType]: e.target.value });
|
||||||
|
}}
|
||||||
|
onBlur={() =>
|
||||||
|
updateInputHandler(inputType, ArrayInputOperations.APPEND)
|
||||||
|
}
|
||||||
|
onKeyPress={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
updateInputHandler(inputType, ArrayInputOperations.APPEND);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
marginLeft="0.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setInputFieldVisibility({
|
||||||
|
...inputFieldVisibility,
|
||||||
|
[inputType]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme="gray"
|
||||||
|
minW="fit-content"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</Tag>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(SelectInputType).includes(inputType)) {
|
||||||
|
if (inputType === SelectInputType.JWT_TYPE) {
|
||||||
|
return (
|
||||||
|
<Select size="sm" {...props}>
|
||||||
|
{[variables[inputType]].map((value: string) => (
|
||||||
|
<option value="value" key={value}>
|
||||||
|
{value}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { options, ...rest } = props;
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
size="sm"
|
||||||
|
{...rest}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVariables({ ...variables, [inputType]: e.target.value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.entries(options).map(([key, value]: any) => (
|
||||||
|
<option value={value} key={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(TextAreaInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Textarea
|
||||||
|
{...props}
|
||||||
|
size="lg"
|
||||||
|
value={inputData[inputType]}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setInputData({ ...inputData, [inputType]: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(SwitchInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex w="25%" justifyContent="space-between">
|
||||||
|
<Code h="75%">Off</Code>
|
||||||
|
<Switch
|
||||||
|
size="md"
|
||||||
|
isChecked={variables[inputType]}
|
||||||
|
onChange={() => {
|
||||||
|
setVariables({
|
||||||
|
...variables,
|
||||||
|
[inputType]: !variables[inputType],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Code h="75%">On</Code>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (Object.values(DateInputType).includes(inputType)) {
|
||||||
|
return (
|
||||||
|
<Flex border="1px solid #e2e8f0" w="100%" h="33px" padding="1%">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
style={{ width: '100%', paddingLeft: '2.5%' }}
|
||||||
|
value={variables[inputType] ? variables[inputType] : ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
setVariables({ ...variables, [inputType]: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InputField;
|
@@ -1,7 +1,6 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
IconButton,
|
IconButton,
|
||||||
Avatar,
|
|
||||||
Box,
|
Box,
|
||||||
CloseButton,
|
CloseButton,
|
||||||
Flex,
|
Flex,
|
||||||
@@ -21,9 +20,7 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import {
|
import {
|
||||||
FiHome,
|
FiHome,
|
||||||
FiTrendingUp,
|
FiCode,
|
||||||
FiCompass,
|
|
||||||
FiStar,
|
|
||||||
FiSettings,
|
FiSettings,
|
||||||
FiMenu,
|
FiMenu,
|
||||||
FiUser,
|
FiUser,
|
||||||
@@ -90,6 +87,17 @@ export const Sidebar = ({ onClose, ...rest }: SidebarProps) => {
|
|||||||
</NavItem>
|
</NavItem>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/playground"
|
||||||
|
target="_blank"
|
||||||
|
style={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
}}
|
||||||
|
_focus={{ _boxShadow: 'none' }}
|
||||||
|
>
|
||||||
|
<NavItem icon={FiCode}>API Playground</NavItem>
|
||||||
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -100,11 +108,6 @@ interface NavItemProps extends FlexProps {
|
|||||||
}
|
}
|
||||||
export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
|
export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Link
|
|
||||||
href="#"
|
|
||||||
style={{ textDecoration: 'none' }}
|
|
||||||
_focus={{ boxShadow: 'none' }}
|
|
||||||
>
|
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
p="3"
|
p="3"
|
||||||
@@ -130,7 +133,6 @@ export const NavItem = ({ icon, children, ...rest }: NavItemProps) => {
|
|||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,6 +163,7 @@ export const MobileNav = ({ onOpen, ...rest }: MobileProps) => {
|
|||||||
borderBottomWidth="1px"
|
borderBottomWidth="1px"
|
||||||
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
|
||||||
justifyContent={{ base: 'space-between', md: 'flex-end' }}
|
justifyContent={{ base: 'space-between', md: 'flex-end' }}
|
||||||
|
zIndex={99}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@@ -1 +1,68 @@
|
|||||||
export const LOGO_URL = "https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png"
|
export const LOGO_URL =
|
||||||
|
'https://user-images.githubusercontent.com/6964334/147834043-fc384cab-e7ca-40f8-9663-38fc25fd5f3a.png';
|
||||||
|
|
||||||
|
export const TextInputType = {
|
||||||
|
GOOGLE_CLIENT_ID: 'GOOGLE_CLIENT_ID',
|
||||||
|
GITHUB_CLIENT_ID: 'GITHUB_CLIENT_ID',
|
||||||
|
FACEBOOK_CLIENT_ID: 'FACEBOOK_CLIENT_ID',
|
||||||
|
JWT_ROLE_CLAIM: 'JWT_ROLE_CLAIM',
|
||||||
|
REDIS_URL: 'REDIS_URL',
|
||||||
|
SMTP_HOST: 'SMTP_HOST',
|
||||||
|
SMTP_PORT: 'SMTP_PORT',
|
||||||
|
SMTP_USERNAME: 'SMTP_USERNAME',
|
||||||
|
SENDER_EMAIL: 'SENDER_EMAIL',
|
||||||
|
ORGANIZATION_NAME: 'ORGANIZATION_NAME',
|
||||||
|
ORGANIZATION_LOGO: 'ORGANIZATION_LOGO',
|
||||||
|
DATABASE_NAME: 'DATABASE_NAME',
|
||||||
|
DATABASE_TYPE: 'DATABASE_TYPE',
|
||||||
|
DATABASE_URL: 'DATABASE_URL',
|
||||||
|
GIVEN_NAME: 'given_name',
|
||||||
|
MIDDLE_NAME: 'middle_name',
|
||||||
|
FAMILY_NAME: 'family_name',
|
||||||
|
NICKNAME: 'nickname',
|
||||||
|
PHONE_NUMBER: 'phone_number',
|
||||||
|
PICTURE: 'picture',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HiddenInputType = {
|
||||||
|
GOOGLE_CLIENT_SECRET: 'GOOGLE_CLIENT_SECRET',
|
||||||
|
GITHUB_CLIENT_SECRET: 'GITHUB_CLIENT_SECRET',
|
||||||
|
FACEBOOK_CLIENT_SECRET: 'FACEBOOK_CLIENT_SECRET',
|
||||||
|
JWT_SECRET: 'JWT_SECRET',
|
||||||
|
SMTP_PASSWORD: 'SMTP_PASSWORD',
|
||||||
|
ADMIN_SECRET: 'ADMIN_SECRET',
|
||||||
|
OLD_ADMIN_SECRET: 'OLD_ADMIN_SECRET',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayInputType = {
|
||||||
|
ROLES: 'ROLES',
|
||||||
|
DEFAULT_ROLES: 'DEFAULT_ROLES',
|
||||||
|
PROTECTED_ROLES: 'PROTECTED_ROLES',
|
||||||
|
ALLOWED_ORIGINS: 'ALLOWED_ORIGINS',
|
||||||
|
USER_ROLES: 'roles',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectInputType = {
|
||||||
|
JWT_TYPE: 'JWT_TYPE',
|
||||||
|
GENDER: 'gender',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextAreaInputType = {
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: 'CUSTOM_ACCESS_TOKEN_SCRIPT',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SwitchInputType = {
|
||||||
|
DISABLE_LOGIN_PAGE: 'DISABLE_LOGIN_PAGE',
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: 'DISABLE_MAGIC_LINK_LOGIN',
|
||||||
|
DISABLE_EMAIL_VERIFICATION: 'DISABLE_EMAIL_VERIFICATION',
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: 'DISABLE_BASIC_AUTHENTICATION',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DateInputType = {
|
||||||
|
BIRTHDATE: 'birthdate',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ArrayInputOperations = {
|
||||||
|
APPEND: 'APPEND',
|
||||||
|
REMOVE: 'REMOVE',
|
||||||
|
};
|
||||||
|
@@ -32,7 +32,7 @@ export const AuthContextProvider = ({ children }: { children: any }) => {
|
|||||||
|
|
||||||
if (fetching) {
|
if (fetching) {
|
||||||
return (
|
return (
|
||||||
<Center>
|
<Center h="100%">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
|
@@ -21,3 +21,27 @@ export const AdminLogout = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const UpdateEnvVariables = `
|
||||||
|
mutation updateEnvVariables($params: UpdateEnvInput!) {
|
||||||
|
_update_env(params: $params) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UpdateUser = `
|
||||||
|
mutation updateUser($params: UpdateUserInput!) {
|
||||||
|
_update_user(params: $params) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DeleteUser = `
|
||||||
|
mutation deleteUser($params: DeleteUserInput!) {
|
||||||
|
_delete_user(params: $params) {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@@ -5,3 +5,69 @@ export const AdminSessionQuery = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const EnvVariablesQuery = `
|
||||||
|
query {
|
||||||
|
_env{
|
||||||
|
GOOGLE_CLIENT_ID,
|
||||||
|
GOOGLE_CLIENT_SECRET,
|
||||||
|
GITHUB_CLIENT_ID,
|
||||||
|
GITHUB_CLIENT_SECRET,
|
||||||
|
FACEBOOK_CLIENT_ID,
|
||||||
|
FACEBOOK_CLIENT_SECRET,
|
||||||
|
ROLES,
|
||||||
|
DEFAULT_ROLES,
|
||||||
|
PROTECTED_ROLES,
|
||||||
|
JWT_TYPE,
|
||||||
|
JWT_SECRET,
|
||||||
|
JWT_ROLE_CLAIM,
|
||||||
|
REDIS_URL,
|
||||||
|
SMTP_HOST,
|
||||||
|
SMTP_PORT,
|
||||||
|
SMTP_USERNAME,
|
||||||
|
SMTP_PASSWORD,
|
||||||
|
SENDER_EMAIL,
|
||||||
|
ALLOWED_ORIGINS,
|
||||||
|
ORGANIZATION_NAME,
|
||||||
|
ORGANIZATION_LOGO,
|
||||||
|
ADMIN_SECRET,
|
||||||
|
DISABLE_LOGIN_PAGE,
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN,
|
||||||
|
DISABLE_EMAIL_VERIFICATION,
|
||||||
|
DISABLE_BASIC_AUTHENTICATION,
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT,
|
||||||
|
DATABASE_NAME,
|
||||||
|
DATABASE_TYPE,
|
||||||
|
DATABASE_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const UserDetailsQuery = `
|
||||||
|
query($params: PaginatedInput) {
|
||||||
|
_users(params: $params) {
|
||||||
|
pagination {
|
||||||
|
limit
|
||||||
|
page
|
||||||
|
offset
|
||||||
|
total
|
||||||
|
}
|
||||||
|
users {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
email_verified
|
||||||
|
given_name
|
||||||
|
family_name
|
||||||
|
middle_name
|
||||||
|
nickname
|
||||||
|
gender
|
||||||
|
birthdate
|
||||||
|
phone_number
|
||||||
|
picture
|
||||||
|
signup_methods
|
||||||
|
roles
|
||||||
|
created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
@@ -1,35 +1,773 @@
|
|||||||
import { Box, Divider, Flex } from '@chakra-ui/react';
|
import React, { useEffect } from 'react';
|
||||||
import React from 'react';
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Stack,
|
||||||
|
Center,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputRightElement,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import {
|
||||||
|
FaGoogle,
|
||||||
|
FaGithub,
|
||||||
|
FaFacebookF,
|
||||||
|
FaSave,
|
||||||
|
FaRegEyeSlash,
|
||||||
|
FaRegEye,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import InputField from '../components/InputField';
|
||||||
|
import { EnvVariablesQuery } from '../graphql/queries';
|
||||||
|
import {
|
||||||
|
ArrayInputType,
|
||||||
|
SelectInputType,
|
||||||
|
HiddenInputType,
|
||||||
|
TextInputType,
|
||||||
|
TextAreaInputType,
|
||||||
|
SwitchInputType,
|
||||||
|
} from '../constants';
|
||||||
|
import { UpdateEnvVariables } from '../graphql/mutation';
|
||||||
|
import { getObjectDiff, capitalizeFirstLetter } from '../utils';
|
||||||
|
|
||||||
|
interface envVarTypes {
|
||||||
|
GOOGLE_CLIENT_ID: string;
|
||||||
|
GOOGLE_CLIENT_SECRET: string;
|
||||||
|
GITHUB_CLIENT_ID: string;
|
||||||
|
GITHUB_CLIENT_SECRET: string;
|
||||||
|
FACEBOOK_CLIENT_ID: string;
|
||||||
|
FACEBOOK_CLIENT_SECRET: string;
|
||||||
|
ROLES: [string] | [];
|
||||||
|
DEFAULT_ROLES: [string] | [];
|
||||||
|
PROTECTED_ROLES: [string] | [];
|
||||||
|
JWT_TYPE: string;
|
||||||
|
JWT_SECRET: string;
|
||||||
|
JWT_ROLE_CLAIM: string;
|
||||||
|
REDIS_URL: string;
|
||||||
|
SMTP_HOST: string;
|
||||||
|
SMTP_PORT: string;
|
||||||
|
SMTP_USERNAME: string;
|
||||||
|
SMTP_PASSWORD: string;
|
||||||
|
SENDER_EMAIL: string;
|
||||||
|
ALLOWED_ORIGINS: [string] | [];
|
||||||
|
ORGANIZATION_NAME: string;
|
||||||
|
ORGANIZATION_LOGO: string;
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: string;
|
||||||
|
ADMIN_SECRET: string;
|
||||||
|
DISABLE_LOGIN_PAGE: boolean;
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: boolean;
|
||||||
|
DISABLE_EMAIL_VERIFICATION: boolean;
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: boolean;
|
||||||
|
OLD_ADMIN_SECRET: string;
|
||||||
|
DATABASE_NAME: string;
|
||||||
|
DATABASE_TYPE: string;
|
||||||
|
DATABASE_URL: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't allow changing database from here as it can cause persistence issues
|
|
||||||
export default function Environment() {
|
export default function Environment() {
|
||||||
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const [adminSecret, setAdminSecret] = React.useState<
|
||||||
|
Record<string, string | boolean>
|
||||||
|
>({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = React.useState<boolean>(true);
|
||||||
|
const [envVariables, setEnvVariables] = React.useState<envVarTypes>({
|
||||||
|
GOOGLE_CLIENT_ID: '',
|
||||||
|
GOOGLE_CLIENT_SECRET: '',
|
||||||
|
GITHUB_CLIENT_ID: '',
|
||||||
|
GITHUB_CLIENT_SECRET: '',
|
||||||
|
FACEBOOK_CLIENT_ID: '',
|
||||||
|
FACEBOOK_CLIENT_SECRET: '',
|
||||||
|
ROLES: [],
|
||||||
|
DEFAULT_ROLES: [],
|
||||||
|
PROTECTED_ROLES: [],
|
||||||
|
JWT_TYPE: '',
|
||||||
|
JWT_SECRET: '',
|
||||||
|
JWT_ROLE_CLAIM: '',
|
||||||
|
REDIS_URL: '',
|
||||||
|
SMTP_HOST: '',
|
||||||
|
SMTP_PORT: '',
|
||||||
|
SMTP_USERNAME: '',
|
||||||
|
SMTP_PASSWORD: '',
|
||||||
|
SENDER_EMAIL: '',
|
||||||
|
ALLOWED_ORIGINS: [],
|
||||||
|
ORGANIZATION_NAME: '',
|
||||||
|
ORGANIZATION_LOGO: '',
|
||||||
|
CUSTOM_ACCESS_TOKEN_SCRIPT: '',
|
||||||
|
ADMIN_SECRET: '',
|
||||||
|
DISABLE_LOGIN_PAGE: false,
|
||||||
|
DISABLE_MAGIC_LINK_LOGIN: false,
|
||||||
|
DISABLE_EMAIL_VERIFICATION: false,
|
||||||
|
DISABLE_BASIC_AUTHENTICATION: false,
|
||||||
|
OLD_ADMIN_SECRET: '',
|
||||||
|
DATABASE_NAME: '',
|
||||||
|
DATABASE_TYPE: '',
|
||||||
|
DATABASE_URL: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [fieldVisibility, setFieldVisibility] = React.useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({
|
||||||
|
GOOGLE_CLIENT_SECRET: false,
|
||||||
|
GITHUB_CLIENT_SECRET: false,
|
||||||
|
FACEBOOK_CLIENT_SECRET: false,
|
||||||
|
JWT_SECRET: false,
|
||||||
|
SMTP_PASSWORD: false,
|
||||||
|
ADMIN_SECRET: false,
|
||||||
|
OLD_ADMIN_SECRET: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
async function getData() {
|
||||||
|
const {
|
||||||
|
data: { _env: envData },
|
||||||
|
} = await client.query(EnvVariablesQuery).toPromise();
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setLoading(false);
|
||||||
|
setEnvVariables({
|
||||||
|
...envData,
|
||||||
|
OLD_ADMIN_SECRET: envData.ADMIN_SECRET,
|
||||||
|
ADMIN_SECRET: '',
|
||||||
|
});
|
||||||
|
setAdminSecret({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getData();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateAdminSecretHandler = (event: any) => {
|
||||||
|
if (envVariables.OLD_ADMIN_SECRET === event.target.value) {
|
||||||
|
setAdminSecret({
|
||||||
|
...adminSecret,
|
||||||
|
value: event.target.value,
|
||||||
|
disableInputField: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAdminSecret({
|
||||||
|
...adminSecret,
|
||||||
|
value: event.target.value,
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (envVariables.ADMIN_SECRET !== '') {
|
||||||
|
setEnvVariables({ ...envVariables, ADMIN_SECRET: '' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveHandler = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const {
|
||||||
|
data: { _env: envData },
|
||||||
|
} = await client.query(EnvVariablesQuery).toPromise();
|
||||||
|
|
||||||
|
const diff = getObjectDiff(envVariables, envData);
|
||||||
|
const updatedEnvVariables = diff.reduce(
|
||||||
|
(acc: any, property: string) => ({
|
||||||
|
...acc,
|
||||||
|
// @ts-ignore
|
||||||
|
[property]: envVariables[property],
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
updatedEnvVariables[HiddenInputType.ADMIN_SECRET] === '' ||
|
||||||
|
updatedEnvVariables[HiddenInputType.OLD_ADMIN_SECRET] !==
|
||||||
|
envData.ADMIN_SECRET
|
||||||
|
) {
|
||||||
|
delete updatedEnvVariables.OLD_ADMIN_SECRET;
|
||||||
|
delete updatedEnvVariables.ADMIN_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete updatedEnvVariables.DATABASE_URL;
|
||||||
|
delete updatedEnvVariables.DATABASE_TYPE;
|
||||||
|
delete updatedEnvVariables.DATABASE_NAME;
|
||||||
|
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateEnvVariables, { params: updatedEnvVariables })
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: capitalizeFirstLetter(res.error.message),
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAdminSecret({
|
||||||
|
value: '',
|
||||||
|
disableInputField: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: `Successfully updated ${
|
||||||
|
Object.keys(updatedEnvVariables).length
|
||||||
|
} variables`,
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m="5" p="5" bg="white" rounded="md">
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
<h1>Social Media Logins</h1>
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
<Divider />- Add horizontal input for clientID and secret for - Google -
|
Social Media Logins
|
||||||
Github - Facebook
|
</Text>
|
||||||
<h1>Roles</h1>
|
<Stack spacing={6} padding="2% 0%">
|
||||||
<Divider />- Add tagged input for roles, default roles, and protected
|
<Flex>
|
||||||
roles
|
<Center
|
||||||
<h1>JWT Configurations</h1>
|
w="50px"
|
||||||
<Divider />- Add input for JWT Type (keep this disabled for now with
|
marginRight="1.5%"
|
||||||
notice saying, "More JWT types will be enabled in upcoming releases"),JWT
|
border="1px solid #e2e8f0"
|
||||||
secret, JWT role claim
|
borderRadius="5px"
|
||||||
<h1>Session Storage</h1>
|
>
|
||||||
<Divider />- Add input for redis url
|
<FaGoogle style={{ color: '#8c8c8c' }} />
|
||||||
<h1>Email Configurations</h1>
|
</Center>
|
||||||
<Divider />- Add input for SMTP Host, PORT, Username, Password, From
|
<Center w="45%" marginRight="1.5%">
|
||||||
Email,
|
<InputField
|
||||||
<h1>White Listing</h1>
|
variables={envVariables}
|
||||||
<Divider />- Add input for allowed origins
|
setVariables={setEnvVariables}
|
||||||
<h1>Organization Information</h1>
|
inputType={TextInputType.GOOGLE_CLIENT_ID}
|
||||||
<Divider />- Add input for organization name, and logo
|
placeholder="Google Client ID"
|
||||||
<h1>Custom Scripts</h1>
|
/>
|
||||||
<Divider />- For now add text area input for CUSTOM_ACCESS_TOKEN_SCRIPT
|
</Center>
|
||||||
<h1>Disable Features</h1>
|
<Center w="45%">
|
||||||
<Divider />
|
<InputField
|
||||||
<h1>Danger</h1>
|
variables={envVariables}
|
||||||
<Divider />- Include changing admin secret
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.GOOGLE_CLIENT_SECRET}
|
||||||
|
placeholder="Google Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Center
|
||||||
|
w="50px"
|
||||||
|
marginRight="1.5%"
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<FaGithub style={{ color: '#8c8c8c' }} />
|
||||||
|
</Center>
|
||||||
|
<Center w="45%" marginRight="1.5%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.GITHUB_CLIENT_ID}
|
||||||
|
placeholder="Github Client ID"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center w="45%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.GITHUB_CLIENT_SECRET}
|
||||||
|
placeholder="Github Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Center
|
||||||
|
w="50px"
|
||||||
|
marginRight="1.5%"
|
||||||
|
border="1px solid #e2e8f0"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<FaFacebookF style={{ color: '#8c8c8c' }} />
|
||||||
|
</Center>
|
||||||
|
<Center w="45%" marginRight="1.5%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.FACEBOOK_CLIENT_ID}
|
||||||
|
placeholder="Facebook Client ID"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
<Center w="45%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.FACEBOOK_CLIENT_SECRET}
|
||||||
|
placeholder="Facebook Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Roles
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Default Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.DEFAULT_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Protected Roles:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.PROTECTED_ROLES}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
JWT (JSON Web Tokens) Configurations
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Type:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<Flex w="100%" justifyContent="space-between">
|
||||||
|
<Flex flex="2">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SelectInputType.JWT_TYPE}
|
||||||
|
isDisabled={true}
|
||||||
|
defaultValue={SelectInputType.JWT_TYPE}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex="3" justifyContent="center" alignItems="center">
|
||||||
|
<Text fontSize="sm">
|
||||||
|
More JWT types will be enabled in upcoming releases.
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Secret</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.JWT_SECRET}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">JWT Role Claim:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.JWT_ROLE_CLAIM}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Session Storage
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Redis URL:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.REDIS_URL}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Email Configurations
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">SMTP Host:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_HOST}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Port:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_PORT}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Username:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SMTP_USERNAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Password:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
inputType={HiddenInputType.SMTP_PASSWORD}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">From Email:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.SENDER_EMAIL}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
White Listing
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Allowed Origins:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={ArrayInputType.ALLOWED_ORIGINS}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Organization Information
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Organization Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.ORGANIZATION_NAME}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Organization Logo:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.ORGANIZATION_LOGO}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Custom Scripts
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Center w="100%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextAreaInputType.CUSTOM_ACCESS_TOKEN_SCRIPT}
|
||||||
|
placeholder="Add script here"
|
||||||
|
minH="25vh"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Disable Features
|
||||||
|
</Text>
|
||||||
|
<Stack spacing={6} padding="2% 0%">
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Login Page:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_LOGIN_PAGE}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Email Verification:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_EMAIL_VERIFICATION}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Magic Login Link:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_MAGIC_LINK_LOGIN}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Disable Basic Authentication:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex justifyContent="start" w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={SwitchInputType.DISABLE_BASIC_AUTHENTICATION}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="2%" marginBottom="2%" />
|
||||||
|
<Text fontSize="md" paddingTop="2%" fontWeight="bold">
|
||||||
|
Danger
|
||||||
|
</Text>
|
||||||
|
<Stack
|
||||||
|
spacing={6}
|
||||||
|
padding="0 5%"
|
||||||
|
marginTop="3%"
|
||||||
|
border="1px solid #ff7875"
|
||||||
|
borderRadius="5px"
|
||||||
|
>
|
||||||
|
<Stack spacing={6} padding="3% 0">
|
||||||
|
<Text fontStyle="italic" fontSize="sm" color="gray.600">
|
||||||
|
Note: Database related environment variables cannot be updated from
|
||||||
|
dashboard :(
|
||||||
|
</Text>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase Name:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_NAME}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase Type:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_TYPE}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex>
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">DataBase URL:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={TextInputType.DATABASE_URL}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Flex marginTop="3%">
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">Old Admin Secret:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputGroup size="sm">
|
||||||
|
<Input
|
||||||
|
size="sm"
|
||||||
|
placeholder="Enter Old Admin Secret"
|
||||||
|
value={adminSecret.value as string}
|
||||||
|
onChange={(event: any) => validateAdminSecretHandler(event)}
|
||||||
|
type={
|
||||||
|
!fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET]
|
||||||
|
? 'password'
|
||||||
|
: 'text'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputRightElement
|
||||||
|
right="5px"
|
||||||
|
children={
|
||||||
|
<Flex>
|
||||||
|
{fieldVisibility[HiddenInputType.OLD_ADMIN_SECRET] ? (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[HiddenInputType.OLD_ADMIN_SECRET]: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEyeSlash color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
) : (
|
||||||
|
<Center
|
||||||
|
w="25px"
|
||||||
|
margin="0 1.5%"
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setFieldVisibility({
|
||||||
|
...fieldVisibility,
|
||||||
|
[HiddenInputType.OLD_ADMIN_SECRET]: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FaRegEye color="#bfbfbf" />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
<Flex paddingBottom="3%">
|
||||||
|
<Flex w="30%" justifyContent="start" alignItems="center">
|
||||||
|
<Text fontSize="sm">New Admin Secret:</Text>
|
||||||
|
</Flex>
|
||||||
|
<Center w="70%">
|
||||||
|
<InputField
|
||||||
|
variables={envVariables}
|
||||||
|
setVariables={setEnvVariables}
|
||||||
|
inputType={HiddenInputType.ADMIN_SECRET}
|
||||||
|
fieldVisibility={fieldVisibility}
|
||||||
|
setFieldVisibility={setFieldVisibility}
|
||||||
|
isDisabled={adminSecret.disableInputField}
|
||||||
|
placeholder="Enter New Admin Secret"
|
||||||
|
/>
|
||||||
|
</Center>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
|
<Divider marginTop="5%" marginBottom="2%" />
|
||||||
|
<Stack spacing={6} padding="1% 0">
|
||||||
|
<Flex justifyContent="end" alignItems="center">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FaSave />}
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="solid"
|
||||||
|
onClick={saveHandler}
|
||||||
|
isDisabled={loading}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,400 @@
|
|||||||
import { Box } from '@chakra-ui/react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useClient } from 'urql';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Flex,
|
||||||
|
IconButton,
|
||||||
|
NumberDecrementStepper,
|
||||||
|
NumberIncrementStepper,
|
||||||
|
NumberInput,
|
||||||
|
NumberInputField,
|
||||||
|
NumberInputStepper,
|
||||||
|
Select,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Text,
|
||||||
|
TableCaption,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tooltip,
|
||||||
|
Tr,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
useToast,
|
||||||
|
Spinner,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
FaAngleLeft,
|
||||||
|
FaAngleRight,
|
||||||
|
FaAngleDoubleLeft,
|
||||||
|
FaAngleDoubleRight,
|
||||||
|
FaExclamationCircle,
|
||||||
|
FaAngleDown,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
import { UserDetailsQuery } from '../graphql/queries';
|
||||||
|
import { UpdateUser } from '../graphql/mutation';
|
||||||
|
import EditUserModal from '../components/EditUserModal';
|
||||||
|
import DeleteUserModal from '../components/DeleteUserModal';
|
||||||
|
|
||||||
|
interface paginationPropTypes {
|
||||||
|
limit: number;
|
||||||
|
page: number;
|
||||||
|
offset: number;
|
||||||
|
total: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface userDataTypes {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
email_verified: boolean;
|
||||||
|
given_name: string;
|
||||||
|
family_name: string;
|
||||||
|
middle_name: string;
|
||||||
|
nickname: string;
|
||||||
|
gender: string;
|
||||||
|
birthdate: string;
|
||||||
|
phone_number: string;
|
||||||
|
picture: string;
|
||||||
|
signup_methods: string;
|
||||||
|
roles: [string];
|
||||||
|
created_at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMaxPages = (pagination: paginationPropTypes) => {
|
||||||
|
const { limit, total } = pagination;
|
||||||
|
if (total > 1) {
|
||||||
|
return total % limit === 0
|
||||||
|
? total / limit
|
||||||
|
: parseInt(`${total / limit}`) + 1;
|
||||||
|
} else return 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLimits = (pagination: paginationPropTypes) => {
|
||||||
|
const { total } = pagination;
|
||||||
|
const limits = [5];
|
||||||
|
if (total > 10) {
|
||||||
|
for (let i = 10; i <= total && limits.length <= 10; i += 5) {
|
||||||
|
limits.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return limits;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
return <Box>Welcome to Users Page</Box>;
|
const client = useClient();
|
||||||
|
const toast = useToast();
|
||||||
|
const [paginationProps, setPaginationProps] =
|
||||||
|
React.useState<paginationPropTypes>({
|
||||||
|
limit: 5,
|
||||||
|
page: 1,
|
||||||
|
offset: 0,
|
||||||
|
total: 0,
|
||||||
|
maxPages: 1,
|
||||||
|
});
|
||||||
|
const [userList, setUserList] = React.useState<userDataTypes[]>([]);
|
||||||
|
const [loading, setLoading] = React.useState<boolean>(false);
|
||||||
|
const updateUserList = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const { data } = await client
|
||||||
|
.query(UserDetailsQuery, {
|
||||||
|
params: {
|
||||||
|
pagination: {
|
||||||
|
limit: paginationProps.limit,
|
||||||
|
page: paginationProps.page,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
if (data?._users) {
|
||||||
|
const { pagination, users } = data._users;
|
||||||
|
const maxPages = getMaxPages(pagination);
|
||||||
|
if (users && users.length > 0) {
|
||||||
|
setPaginationProps({ ...paginationProps, ...pagination, maxPages });
|
||||||
|
setUserList(users);
|
||||||
|
} else {
|
||||||
|
if (paginationProps.page !== 1) {
|
||||||
|
setPaginationProps({
|
||||||
|
...paginationProps,
|
||||||
|
...pagination,
|
||||||
|
maxPages,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateUserList();
|
||||||
|
}, []);
|
||||||
|
React.useEffect(() => {
|
||||||
|
updateUserList();
|
||||||
|
}, [paginationProps.page, paginationProps.limit]);
|
||||||
|
|
||||||
|
const paginationHandler = (value: Record<string, number>) => {
|
||||||
|
setPaginationProps({ ...paginationProps, ...value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const userVerificationHandler = async (user: userDataTypes) => {
|
||||||
|
const { id, email } = user;
|
||||||
|
const res = await client
|
||||||
|
.mutation(UpdateUser, {
|
||||||
|
params: {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
email_verified: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
if (res.error) {
|
||||||
|
toast({
|
||||||
|
title: 'User verification failed',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'error',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
} else if (res.data?._update_user?.id) {
|
||||||
|
toast({
|
||||||
|
title: 'User verification successful',
|
||||||
|
isClosable: true,
|
||||||
|
status: 'success',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateUserList();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box m="5" py="5" px="10" bg="white" rounded="md">
|
||||||
|
<Flex margin="2% 0" justifyContent="space-between" alignItems="center">
|
||||||
|
<Text fontSize="md" fontWeight="bold">
|
||||||
|
Users
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
{!loading ? (
|
||||||
|
userList.length > 0 ? (
|
||||||
|
<Table variant="simple">
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Email</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Signup Methods</Th>
|
||||||
|
<Th>Roles</Th>
|
||||||
|
<Th>Verified</Th>
|
||||||
|
<Th>Actions</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{userList.map((user: userDataTypes) => {
|
||||||
|
const { email_verified, created_at, ...rest }: any = user;
|
||||||
|
return (
|
||||||
|
<Tr key={user.id} style={{ fontSize: 14 }}>
|
||||||
|
<Td>{user.email}</Td>
|
||||||
|
<Td>
|
||||||
|
{dayjs(user.created_at * 1000).format('MMM DD, YYYY')}
|
||||||
|
</Td>
|
||||||
|
<Td>{user.signup_methods}</Td>
|
||||||
|
<Td>{user.roles.join(', ')}</Td>
|
||||||
|
<Td>
|
||||||
|
<Tag
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={user.email_verified ? 'green' : 'yellow'}
|
||||||
|
>
|
||||||
|
{user.email_verified.toString()}
|
||||||
|
</Tag>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} variant="unstyled" size="sm">
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text fontSize="sm" fontWeight="light">
|
||||||
|
Menu
|
||||||
|
</Text>
|
||||||
|
<FaAngleDown style={{ marginLeft: 10 }} />
|
||||||
|
</Flex>
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
{!user.email_verified && (
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => userVerificationHandler(user)}
|
||||||
|
>
|
||||||
|
Verify User
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<EditUserModal
|
||||||
|
user={rest}
|
||||||
|
updateUserList={updateUserList}
|
||||||
|
/>
|
||||||
|
<DeleteUserModal
|
||||||
|
user={rest}
|
||||||
|
updateUserList={updateUserList}
|
||||||
|
/>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tbody>
|
||||||
|
{(paginationProps.maxPages > 1 || paginationProps.total >= 5) && (
|
||||||
|
<TableCaption>
|
||||||
|
<Flex
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center"
|
||||||
|
m="2% 0"
|
||||||
|
>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="First Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
mr={4}
|
||||||
|
icon={<FaAngleDoubleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Previous Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={paginationProps.page <= 1}
|
||||||
|
icon={<FaAngleLeft />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
<Flex
|
||||||
|
flex="8"
|
||||||
|
justifyContent="space-evenly"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Text mr={8}>
|
||||||
|
Page{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.page}
|
||||||
|
</Text>{' '}
|
||||||
|
of{' '}
|
||||||
|
<Text fontWeight="bold" as="span">
|
||||||
|
{paginationProps.maxPages}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Flex alignItems="center">
|
||||||
|
<Text flexShrink="0">Go to page:</Text>{' '}
|
||||||
|
<NumberInput
|
||||||
|
ml={2}
|
||||||
|
mr={8}
|
||||||
|
w={28}
|
||||||
|
min={1}
|
||||||
|
max={paginationProps.maxPages}
|
||||||
|
onChange={(value) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: parseInt(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
value={paginationProps.page}
|
||||||
|
>
|
||||||
|
<NumberInputField />
|
||||||
|
<NumberInputStepper>
|
||||||
|
<NumberIncrementStepper />
|
||||||
|
<NumberDecrementStepper />
|
||||||
|
</NumberInputStepper>
|
||||||
|
</NumberInput>
|
||||||
|
</Flex>
|
||||||
|
<Select
|
||||||
|
w={32}
|
||||||
|
value={paginationProps.limit}
|
||||||
|
onChange={(e) =>
|
||||||
|
paginationHandler({
|
||||||
|
page: 1,
|
||||||
|
limit: parseInt(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{getLimits(paginationProps).map((pageSize) => (
|
||||||
|
<option key={pageSize} value={pageSize}>
|
||||||
|
Show {pageSize}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex="1">
|
||||||
|
<Tooltip label="Next Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.page + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
icon={<FaAngleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Last Page">
|
||||||
|
<IconButton
|
||||||
|
aria-label="icon button"
|
||||||
|
onClick={() =>
|
||||||
|
paginationHandler({
|
||||||
|
page: paginationProps.maxPages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
isDisabled={
|
||||||
|
paginationProps.page >= paginationProps.maxPages
|
||||||
|
}
|
||||||
|
ml={4}
|
||||||
|
icon={<FaAngleDoubleRight />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</TableCaption>
|
||||||
|
)}
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
flexDirection="column"
|
||||||
|
minH="25vh"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Center w="50px" marginRight="1.5%">
|
||||||
|
<FaExclamationCircle style={{ color: '#f0f0f0', fontSize: 70 }} />
|
||||||
|
</Center>
|
||||||
|
<Text
|
||||||
|
fontSize="2xl"
|
||||||
|
paddingRight="1%"
|
||||||
|
fontWeight="bold"
|
||||||
|
color="#d9d9d9"
|
||||||
|
>
|
||||||
|
No Data
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Center minH="25vh">
|
||||||
|
<Spinner />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@@ -26,6 +26,7 @@ export const AppRoutes = () => {
|
|||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="users" element={<Users />} />
|
<Route path="users" element={<Users />} />
|
||||||
<Route path="environment" element={<Environment />} />
|
<Route path="environment" element={<Environment />} />
|
||||||
|
<Route path="*" element={<Home />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -35,6 +36,7 @@ export const AppRoutes = () => {
|
|||||||
<Suspense fallback={<></>}>
|
<Suspense fallback={<></>}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Auth />} />
|
<Route path="/" element={<Auth />} />
|
||||||
|
<Route path="*" element={<Auth />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
@@ -1,6 +1,66 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const hasAdminSecret = () => {
|
export const hasAdminSecret = () => {
|
||||||
return (<any>window)['__authorizer__'].isOnboardingCompleted === true;
|
return (<any>window)['__authorizer__'].isOnboardingCompleted === true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const capitalizeFirstLetter = (data: string): string =>
|
export const capitalizeFirstLetter = (data: string): string =>
|
||||||
data.charAt(0).toUpperCase() + data.slice(1);
|
data.charAt(0).toUpperCase() + data.slice(1);
|
||||||
|
|
||||||
|
const fallbackCopyTextToClipboard = (text: string) => {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
|
||||||
|
textArea.value = text;
|
||||||
|
textArea.style.top = '0';
|
||||||
|
textArea.style.left = '0';
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.focus();
|
||||||
|
textArea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const successful = document.execCommand('copy');
|
||||||
|
const msg = successful ? 'successful' : 'unsuccessful';
|
||||||
|
console.log('Fallback: Copying text command was ' + msg);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback: Oops, unable to copy', err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const copyTextToClipboard = (text: string) => {
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
fallbackCopyTextToClipboard(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(text).then(
|
||||||
|
() => {
|
||||||
|
console.log('Async: Copying to clipboard was successful!');
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Async: Could not copy text: ', err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getObjectDiff = (obj1: any, obj2: any) => {
|
||||||
|
const diff = Object.keys(obj1).reduce((result, key) => {
|
||||||
|
if (!obj2.hasOwnProperty(key)) {
|
||||||
|
result.push(key);
|
||||||
|
} else if (
|
||||||
|
_.isEqual(obj1[key], obj2[key]) ||
|
||||||
|
(obj1[key] === null && obj2[key] === '') ||
|
||||||
|
(obj1[key] &&
|
||||||
|
Array.isArray(obj1[key]) &&
|
||||||
|
obj1[key].length === 0 &&
|
||||||
|
obj2[key] === null)
|
||||||
|
) {
|
||||||
|
const resultKeyIndex = result.indexOf(key);
|
||||||
|
result.splice(resultKeyIndex, 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, Object.keys(obj2));
|
||||||
|
|
||||||
|
return diff;
|
||||||
|
};
|
||||||
|
@@ -67,6 +67,7 @@
|
|||||||
|
|
||||||
/* Advanced Options */
|
/* Advanced Options */
|
||||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||||
|
"lib": ["esnext", "dom"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ const (
|
|||||||
// EnvKeyVersion key for build arg version
|
// EnvKeyVersion key for build arg version
|
||||||
EnvKeyVersion = "VERSION"
|
EnvKeyVersion = "VERSION"
|
||||||
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
|
// EnvKeyAuthorizerURL key for env variable AUTHORIZER_URL
|
||||||
|
// TODO: remove support AUTHORIZER_URL env
|
||||||
EnvKeyAuthorizerURL = "AUTHORIZER_URL"
|
EnvKeyAuthorizerURL = "AUTHORIZER_URL"
|
||||||
// EnvKeyPort key for env variable PORT
|
// EnvKeyPort key for env variable PORT
|
||||||
EnvKeyPort = "PORT"
|
EnvKeyPort = "PORT"
|
||||||
|
4
server/constants/pagination.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
// DefaultLimit is the default limit for pagination
|
||||||
|
var DefaultLimit = 10
|
@@ -13,7 +13,8 @@ import (
|
|||||||
func SetAdminCookie(gc *gin.Context, token string) {
|
func SetAdminCookie(gc *gin.Context, token string) {
|
||||||
secure := true
|
secure := true
|
||||||
httpOnly := true
|
httpOnly := true
|
||||||
host, _ := utils.GetHostParts(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
|
||||||
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), token, 3600, "/", host, secure, httpOnly)
|
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), token, 3600, "/", host, secure, httpOnly)
|
||||||
}
|
}
|
||||||
@@ -38,7 +39,8 @@ func GetAdminCookie(gc *gin.Context) (string, error) {
|
|||||||
func DeleteAdminCookie(gc *gin.Context) {
|
func DeleteAdminCookie(gc *gin.Context) {
|
||||||
secure := true
|
secure := true
|
||||||
httpOnly := true
|
httpOnly := true
|
||||||
host, _ := utils.GetHostParts(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
hostname := utils.GetHost(gc)
|
||||||
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
|
||||||
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), "", -1, "/", host, secure, httpOnly)
|
gc.SetCookie(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), "", -1, "/", host, secure, httpOnly)
|
||||||
}
|
}
|
||||||
|
@@ -19,8 +19,9 @@ import (
|
|||||||
func SetCookie(gc *gin.Context, accessToken, refreshToken, fingerprintHash string) {
|
func SetCookie(gc *gin.Context, accessToken, refreshToken, fingerprintHash string) {
|
||||||
secure := true
|
secure := true
|
||||||
httpOnly := true
|
httpOnly := true
|
||||||
host, _ := utils.GetHostParts(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
hostname := utils.GetHost(gc)
|
||||||
domain := utils.GetDomainName(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
host, _ := utils.GetHostParts(hostname)
|
||||||
|
domain := utils.GetDomainName(hostname)
|
||||||
if domain != "localhost" {
|
if domain != "localhost" {
|
||||||
domain = "." + domain
|
domain = "." + domain
|
||||||
}
|
}
|
||||||
@@ -86,9 +87,9 @@ func GetFingerPrintCookie(gc *gin.Context) (string, error) {
|
|||||||
func DeleteCookie(gc *gin.Context) {
|
func DeleteCookie(gc *gin.Context) {
|
||||||
secure := true
|
secure := true
|
||||||
httpOnly := true
|
httpOnly := true
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
host, _ := utils.GetHostParts(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
host, _ := utils.GetHostParts(hostname)
|
||||||
domain := utils.GetDomainName(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL))
|
domain := utils.GetDomainName(hostname)
|
||||||
if domain != "localhost" {
|
if domain != "localhost" {
|
||||||
domain = "." + domain
|
domain = "." + domain
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,8 @@ package models
|
|||||||
type Env struct {
|
type Env struct {
|
||||||
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
EnvData []byte `gorm:"type:text" json:"env" bson:"env"`
|
EnvData string `gorm:"type:text" json:"env" bson:"env"`
|
||||||
Hash string `gorm:"type:hash" json:"hash" bson:"hash"`
|
Hash string `gorm:"type:text" json:"hash" bson:"hash"`
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
}
|
}
|
||||||
|
@@ -5,9 +5,9 @@ type Session struct {
|
|||||||
Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
|
Key string `json:"_key,omitempty" bson:"_key,omitempty"` // for arangodb
|
||||||
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
ID string `gorm:"primaryKey;type:char(36)" json:"_id" bson:"_id"`
|
||||||
UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"`
|
UserID string `gorm:"type:char(36),index:" json:"user_id" bson:"user_id"`
|
||||||
User User `json:"-" bson:"-"`
|
User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"-" bson:"-"`
|
||||||
UserAgent string `json:"user_agent" bson:"user_agent"`
|
UserAgent string `json:"user_agent" bson:"user_agent"`
|
||||||
IP string `json:"ip" bson:"ip"`
|
IP string `json:"ip" bson:"ip"`
|
||||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
}
|
}
|
||||||
|
@@ -25,8 +25,8 @@ type User struct {
|
|||||||
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"`
|
PhoneNumberVerifiedAt *int64 `json:"phone_number_verified_at" bson:"phone_number_verified_at"`
|
||||||
Picture *string `gorm:"type:text" json:"picture" bson:"picture"`
|
Picture *string `gorm:"type:text" json:"picture" bson:"picture"`
|
||||||
Roles string `json:"roles" bson:"roles"`
|
Roles string `json:"roles" bson:"roles"`
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) AsAPIUser() *model.User {
|
func (user *User) AsAPIUser() *model.User {
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
|
import "github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
|
||||||
// VerificationRequest model for db
|
// VerificationRequest model for db
|
||||||
type VerificationRequest struct {
|
type VerificationRequest struct {
|
||||||
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
Key string `json:"_key,omitempty" bson:"_key"` // for arangodb
|
||||||
@@ -7,7 +9,19 @@ type VerificationRequest struct {
|
|||||||
Token string `gorm:"type:text" json:"token" bson:"token"`
|
Token string `gorm:"type:text" json:"token" bson:"token"`
|
||||||
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
|
Identifier string `gorm:"uniqueIndex:idx_email_identifier" json:"identifier" bson:"identifier"`
|
||||||
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
|
ExpiresAt int64 `json:"expires_at" bson:"expires_at"`
|
||||||
CreatedAt int64 `gorm:"autoCreateTime" json:"created_at" bson:"created_at"`
|
CreatedAt int64 `json:"created_at" bson:"created_at"`
|
||||||
UpdatedAt int64 `gorm:"autoUpdateTime" json:"updated_at" bson:"updated_at"`
|
UpdatedAt int64 `json:"updated_at" bson:"updated_at"`
|
||||||
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
Email string `gorm:"uniqueIndex:idx_email_identifier" json:"email" bson:"email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *VerificationRequest) AsAPIVerificationRequest() *model.VerificationRequest {
|
||||||
|
return &model.VerificationRequest{
|
||||||
|
ID: v.ID,
|
||||||
|
Token: &v.Token,
|
||||||
|
Identifier: &v.Identifier,
|
||||||
|
Expires: &v.ExpiresAt,
|
||||||
|
CreatedAt: &v.CreatedAt,
|
||||||
|
UpdatedAt: &v.UpdatedAt,
|
||||||
|
Email: &v.Email,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package arangodb
|
package arangodb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,32 +68,40 @@ func (p *provider) DeleteUser(user models.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers to get list of users from database
|
// ListUsers to get list of users from database
|
||||||
func (p *provider) ListUsers() ([]models.User, error) {
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
var users []models.User
|
var users []*model.User
|
||||||
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.User)
|
ctx := driver.WithQueryFullCount(context.Background())
|
||||||
|
|
||||||
cursor, err := p.db.Query(nil, query, nil)
|
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.User, pagination.Offset, pagination.Limit)
|
||||||
|
|
||||||
|
cursor, err := p.db.Query(ctx, query, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer cursor.Close()
|
defer cursor.Close()
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = cursor.Statistics().FullCount()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var user models.User
|
var user models.User
|
||||||
meta, err := cursor.ReadDocument(nil, &user)
|
meta, err := cursor.ReadDocument(nil, &user)
|
||||||
|
|
||||||
if driver.IsNoMoreDocuments(err) {
|
if arangoDriver.IsNoMoreDocuments(err) {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return users, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.Key != "" {
|
if meta.Key != "" {
|
||||||
users = append(users, user)
|
users = append(users, user.AsAPIUser())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: users,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail to get user information from database using email address
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
package arangodb
|
package arangodb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/arangodb/go-driver"
|
"github.com/arangodb/go-driver"
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -93,17 +95,20 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListVerificationRequests to get list of verification requests from database
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) {
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
var verificationRequests []models.VerificationRequest
|
var verificationRequests []*model.VerificationRequest
|
||||||
|
ctx := driver.WithQueryFullCount(context.Background())
|
||||||
|
query := fmt.Sprintf("FOR d in %s SORT d.created_at DESC LIMIT %d, %d RETURN d", models.Collections.VerificationRequest, pagination.Offset, pagination.Limit)
|
||||||
|
|
||||||
query := fmt.Sprintf("FOR d in %s RETURN d", models.Collections.VerificationRequest)
|
cursor, err := p.db.Query(ctx, query, nil)
|
||||||
|
|
||||||
cursor, err := p.db.Query(nil, query, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return verificationRequests, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer cursor.Close()
|
defer cursor.Close()
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = cursor.Statistics().FullCount()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var verificationRequest models.VerificationRequest
|
var verificationRequest models.VerificationRequest
|
||||||
meta, err := cursor.ReadDocument(nil, &verificationRequest)
|
meta, err := cursor.ReadDocument(nil, &verificationRequest)
|
||||||
@@ -111,16 +116,19 @@ func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, err
|
|||||||
if driver.IsNoMoreDocuments(err) {
|
if driver.IsNoMoreDocuments(err) {
|
||||||
break
|
break
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return verificationRequests, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if meta.Key != "" {
|
if meta.Key != "" {
|
||||||
verificationRequests = append(verificationRequests, verificationRequest)
|
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return verificationRequests, nil
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: verificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerificationRequest to delete verification request from database
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
@@ -60,13 +61,29 @@ func (p *provider) DeleteUser(user models.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers to get list of users from database
|
// ListUsers to get list of users from database
|
||||||
func (p *provider) ListUsers() ([]models.User, error) {
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
var users []models.User
|
var users []*model.User
|
||||||
|
opts := options.Find()
|
||||||
|
opts.SetLimit(pagination.Limit)
|
||||||
|
opts.SetSkip(pagination.Offset)
|
||||||
|
opts.SetSort(bson.M{"created_at": -1})
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
// TODO add pagination total
|
||||||
|
|
||||||
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
userCollection := p.db.Collection(models.Collections.User, options.Collection())
|
||||||
cursor, err := userCollection.Find(nil, bson.M{}, options.Find())
|
count, err := userCollection.CountDocuments(nil, bson.M{}, options.Count())
|
||||||
|
if err != nil {
|
||||||
|
log.Println("error getting total users:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone.Total = count
|
||||||
|
|
||||||
|
cursor, err := userCollection.Find(nil, bson.M{}, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error getting users:", err)
|
log.Println("error getting users:", err)
|
||||||
return users, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer cursor.Close(nil)
|
defer cursor.Close(nil)
|
||||||
|
|
||||||
@@ -74,12 +91,15 @@ func (p *provider) ListUsers() ([]models.User, error) {
|
|||||||
var user models.User
|
var user models.User
|
||||||
err := cursor.Decode(&user)
|
err := cursor.Decode(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return users, err
|
return nil, err
|
||||||
}
|
}
|
||||||
users = append(users, user)
|
users = append(users, user.AsAPIUser())
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: users,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail to get user information from database using email address
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.mongodb.org/mongo-driver/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
@@ -56,13 +57,24 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListVerificationRequests to get list of verification requests from database
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) {
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
var verificationRequests []models.VerificationRequest
|
var verificationRequests []*model.VerificationRequest
|
||||||
|
|
||||||
|
opts := options.Find()
|
||||||
|
opts.SetLimit(pagination.Limit)
|
||||||
|
opts.SetSkip(pagination.Offset)
|
||||||
|
opts.SetSort(bson.M{"created_at": -1})
|
||||||
|
|
||||||
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
verificationRequestCollection := p.db.Collection(models.Collections.VerificationRequest, options.Collection())
|
||||||
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, options.Find())
|
|
||||||
|
verificationRequestCollectionCount, err := verificationRequestCollection.CountDocuments(nil, bson.M{})
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = verificationRequestCollectionCount
|
||||||
|
|
||||||
|
cursor, err := verificationRequestCollection.Find(nil, bson.M{}, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error getting verification requests:", err)
|
log.Println("error getting verification requests:", err)
|
||||||
return verificationRequests, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer cursor.Close(nil)
|
defer cursor.Close(nil)
|
||||||
|
|
||||||
@@ -70,12 +82,15 @@ func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, err
|
|||||||
var verificationRequest models.VerificationRequest
|
var verificationRequest models.VerificationRequest
|
||||||
err := cursor.Decode(&verificationRequest)
|
err := cursor.Decode(&verificationRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return verificationRequests, err
|
return nil, err
|
||||||
}
|
}
|
||||||
verificationRequests = append(verificationRequests, verificationRequest)
|
verificationRequests = append(verificationRequests, verificationRequest.AsAPIVerificationRequest())
|
||||||
}
|
}
|
||||||
|
|
||||||
return verificationRequests, nil
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: verificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerificationRequest to delete verification request from database
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
package providers
|
package providers
|
||||||
|
|
||||||
import "github.com/authorizerdev/authorizer/server/db/models"
|
import (
|
||||||
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
type Provider interface {
|
type Provider interface {
|
||||||
// AddUser to save user information in database
|
// AddUser to save user information in database
|
||||||
@@ -10,7 +13,7 @@ type Provider interface {
|
|||||||
// DeleteUser to delete user information from database
|
// DeleteUser to delete user information from database
|
||||||
DeleteUser(user models.User) error
|
DeleteUser(user models.User) error
|
||||||
// ListUsers to get list of users from database
|
// ListUsers to get list of users from database
|
||||||
ListUsers() ([]models.User, error)
|
ListUsers(pagination model.Pagination) (*model.Users, error)
|
||||||
// GetUserByEmail to get user information from database using email address
|
// GetUserByEmail to get user information from database using email address
|
||||||
GetUserByEmail(email string) (models.User, error)
|
GetUserByEmail(email string) (models.User, error)
|
||||||
// GetUserByID to get user information from database using user ID
|
// GetUserByID to get user information from database using user ID
|
||||||
@@ -23,7 +26,7 @@ type Provider interface {
|
|||||||
// GetVerificationRequestByEmail to get verification request by email from database
|
// GetVerificationRequestByEmail to get verification request by email from database
|
||||||
GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error)
|
GetVerificationRequestByEmail(email string, identifier string) (models.VerificationRequest, error)
|
||||||
// ListVerificationRequests to get list of verification requests from database
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
ListVerificationRequests() ([]models.VerificationRequest, error)
|
ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error)
|
||||||
// DeleteVerificationRequest to delete verification request from database
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
DeleteVerificationRequest(verificationRequest models.VerificationRequest) error
|
DeleteVerificationRequest(verificationRequest models.VerificationRequest) error
|
||||||
|
|
||||||
|
@@ -15,8 +15,10 @@ func (p *provider) AddEnv(env models.Env) (models.Env, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
env.Key = env.ID
|
env.Key = env.ID
|
||||||
result := p.db.Create(&env)
|
env.CreatedAt = time.Now().Unix()
|
||||||
|
env.UpdatedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
result := p.db.Create(&env)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.Println("error adding config:", result.Error)
|
log.Println("error adding config:", result.Error)
|
||||||
return env, result.Error
|
return env, result.Error
|
||||||
|
@@ -2,6 +2,7 @@ package sql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -15,6 +16,8 @@ func (p *provider) AddSession(session models.Session) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
session.Key = session.ID
|
session.Key = session.ID
|
||||||
|
session.CreatedAt = time.Now().Unix()
|
||||||
|
session.UpdatedAt = time.Now().Unix()
|
||||||
res := p.db.Clauses(
|
res := p.db.Clauses(
|
||||||
clause.OnConflict{
|
clause.OnConflict{
|
||||||
DoNothing: true,
|
DoNothing: true,
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
@@ -22,6 +23,8 @@ func (p *provider) AddUser(user models.User) (models.User, error) {
|
|||||||
user.Roles = strings.Join(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
user.Roles = strings.Join(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyDefaultRoles), ",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.CreatedAt = time.Now().Unix()
|
||||||
|
user.UpdatedAt = time.Now().Unix()
|
||||||
user.Key = user.ID
|
user.Key = user.ID
|
||||||
result := p.db.Clauses(
|
result := p.db.Clauses(
|
||||||
clause.OnConflict{
|
clause.OnConflict{
|
||||||
@@ -64,15 +67,32 @@ func (p *provider) DeleteUser(user models.User) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers to get list of users from database
|
// ListUsers to get list of users from database
|
||||||
func (p *provider) ListUsers() ([]models.User, error) {
|
func (p *provider) ListUsers(pagination model.Pagination) (*model.Users, error) {
|
||||||
var users []models.User
|
var users []models.User
|
||||||
result := p.db.Find(&users)
|
result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&users)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.Println("error getting users:", result.Error)
|
log.Println("error getting users:", result.Error)
|
||||||
return users, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
responseUsers := []*model.User{}
|
||||||
|
for _, user := range users {
|
||||||
|
responseUsers = append(responseUsers, user.AsAPIUser())
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
totalRes := p.db.Model(&models.User{}).Count(&total)
|
||||||
|
if totalRes.Error != nil {
|
||||||
|
return nil, totalRes.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = total
|
||||||
|
|
||||||
|
return &model.Users{
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
Users: responseUsers,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail to get user information from database using email address
|
// GetUserByEmail to get user information from database using email address
|
||||||
|
@@ -2,8 +2,10 @@ package sql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/db/models"
|
"github.com/authorizerdev/authorizer/server/db/models"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"gorm.io/gorm/clause"
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,8 @@ func (p *provider) AddVerificationRequest(verificationRequest models.Verificatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
verificationRequest.Key = verificationRequest.ID
|
verificationRequest.Key = verificationRequest.ID
|
||||||
|
verificationRequest.CreatedAt = time.Now().Unix()
|
||||||
|
verificationRequest.UpdatedAt = time.Now().Unix()
|
||||||
result := p.db.Clauses(clause.OnConflict{
|
result := p.db.Clauses(clause.OnConflict{
|
||||||
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
|
Columns: []clause.Column{{Name: "email"}, {Name: "identifier"}},
|
||||||
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
|
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at"}),
|
||||||
@@ -56,15 +60,33 @@ func (p *provider) GetVerificationRequestByEmail(email string, identifier string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ListVerificationRequests to get list of verification requests from database
|
// ListVerificationRequests to get list of verification requests from database
|
||||||
func (p *provider) ListVerificationRequests() ([]models.VerificationRequest, error) {
|
func (p *provider) ListVerificationRequests(pagination model.Pagination) (*model.VerificationRequests, error) {
|
||||||
var verificationRequests []models.VerificationRequest
|
var verificationRequests []models.VerificationRequest
|
||||||
|
|
||||||
result := p.db.Find(&verificationRequests)
|
result := p.db.Limit(int(pagination.Limit)).Offset(int(pagination.Offset)).Order("created_at DESC").Find(&verificationRequests)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
log.Println("error getting verification requests:", result.Error)
|
log.Println("error getting verification requests:", result.Error)
|
||||||
return verificationRequests, result.Error
|
return nil, result.Error
|
||||||
}
|
}
|
||||||
return verificationRequests, nil
|
|
||||||
|
responseVerificationRequests := []*model.VerificationRequest{}
|
||||||
|
for _, v := range verificationRequests {
|
||||||
|
responseVerificationRequests = append(responseVerificationRequests, v.AsAPIVerificationRequest())
|
||||||
|
}
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
totalRes := p.db.Model(&models.VerificationRequest{}).Count(&total)
|
||||||
|
if totalRes.Error != nil {
|
||||||
|
return nil, totalRes.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
paginationClone := pagination
|
||||||
|
paginationClone.Total = total
|
||||||
|
|
||||||
|
return &model.VerificationRequests{
|
||||||
|
VerificationRequests: responseVerificationRequests,
|
||||||
|
Pagination: &paginationClone,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerificationRequest to delete verification request from database
|
// DeleteVerificationRequest to delete verification request from database
|
||||||
|
@@ -6,10 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SendForgotPasswordMail to send forgot password email
|
// SendForgotPasswordMail to send forgot password email
|
||||||
func SendForgotPasswordMail(toEmail, token, host string) error {
|
func SendForgotPasswordMail(toEmail, token, hostname string) error {
|
||||||
resetPasswordUrl := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL)
|
resetPasswordUrl := envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyResetPasswordURL)
|
||||||
if resetPasswordUrl == "" {
|
if resetPasswordUrl == "" {
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyResetPasswordURL, envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL)+"/app/reset-password")
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyResetPasswordURL, hostname+"/app/reset-password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// The receiver needs to be in slice as the receive supports multiple receiver
|
// The receiver needs to be in slice as the receive supports multiple receiver
|
||||||
|
@@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// SendVerificationMail to send verification email
|
// SendVerificationMail to send verification email
|
||||||
func SendVerificationMail(toEmail, token string) error {
|
func SendVerificationMail(toEmail, token, hostname string) error {
|
||||||
// The receiver needs to be in slice as the receive supports multiple receiver
|
// The receiver needs to be in slice as the receive supports multiple receiver
|
||||||
Receiver := []string{toEmail}
|
Receiver := []string{toEmail}
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ func SendVerificationMail(toEmail, token string) error {
|
|||||||
data := make(map[string]interface{}, 3)
|
data := make(map[string]interface{}, 3)
|
||||||
data["org_logo"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
|
data["org_logo"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationLogo)
|
||||||
data["org_name"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
|
data["org_name"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyOrganizationName)
|
||||||
data["verification_url"] = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/verify_email?token=" + token
|
data["verification_url"] = hostname + "/verify_email?token=" + token
|
||||||
message = addEmailTemplate(message, data, "verify_email.tmpl")
|
message = addEmailTemplate(message, data, "verify_email.tmpl")
|
||||||
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
|
// bodyMessage := sender.WriteHTMLEmail(Receiver, Subject, message)
|
||||||
|
|
||||||
|
5
server/env/env.go
vendored
@@ -31,8 +31,6 @@ func InitEnv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set authorizer url to empty string so that fresh url is obtained with every server start
|
|
||||||
envData.StringEnv[constants.EnvKeyAuthorizerURL] = ""
|
|
||||||
if envData.StringEnv[constants.EnvKeyAppURL] == "" {
|
if envData.StringEnv[constants.EnvKeyAppURL] == "" {
|
||||||
envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL)
|
envData.StringEnv[constants.EnvKeyAppURL] = os.Getenv(constants.EnvKeyAppURL)
|
||||||
}
|
}
|
||||||
@@ -246,12 +244,11 @@ func InitEnv() {
|
|||||||
trimVal := strings.TrimSpace(val)
|
trimVal := strings.TrimSpace(val)
|
||||||
if trimVal != "" {
|
if trimVal != "" {
|
||||||
roles = append(roles, trimVal)
|
roles = append(roles, trimVal)
|
||||||
}
|
|
||||||
|
|
||||||
if utils.StringSliceContains(defaultRoleSplit, trimVal) {
|
if utils.StringSliceContains(defaultRoleSplit, trimVal) {
|
||||||
defaultRoles = append(defaultRoles, trimVal)
|
defaultRoles = append(defaultRoles, trimVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRolesEnv) > 0 {
|
if len(roles) > 0 && len(defaultRoles) == 0 && len(defaultRolesEnv) > 0 {
|
||||||
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
|
panic(`Invalid DEFAULT_ROLE environment variable. It can be one from give ROLES environment variable value`)
|
||||||
|
21
server/env/persist_env.go
vendored
@@ -25,15 +25,19 @@ func PersistEnv() error {
|
|||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, hash)
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, hash)
|
||||||
encodedHash := utils.EncryptB64(hash)
|
encodedHash := utils.EncryptB64(hash)
|
||||||
|
|
||||||
configData, err := json.Marshal(envstore.EnvInMemoryStoreObj.GetEnvStoreClone())
|
encryptedConfig, err := utils.EncryptEnvData(envstore.EnvInMemoryStoreObj.GetEnvStoreClone())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// configData, err := json.Marshal()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
encryptedConfig, err := utils.EncryptAES(configData)
|
// encryptedConfig, err := utils.EncryptAES(configData)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
env = models.Env{
|
env = models.Env{
|
||||||
Hash: encodedHash,
|
Hash: encodedHash,
|
||||||
@@ -51,7 +55,12 @@ func PersistEnv() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey)
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEncryptionKey, decryptedEncryptionKey)
|
||||||
decryptedConfigs, err := utils.DecryptAES(env.EnvData)
|
b64DecryptedConfig, err := utils.DecryptB64(env.EnvData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedConfigs, err := utils.DecryptAES([]byte(b64DecryptedConfig))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,7 @@ var EnvInMemoryStoreObj = &EnvInMemoryStore{
|
|||||||
constants.EnvKeyAdminCookieName: "authorizer-admin",
|
constants.EnvKeyAdminCookieName: "authorizer-admin",
|
||||||
constants.EnvKeyJwtRoleClaim: "role",
|
constants.EnvKeyJwtRoleClaim: "role",
|
||||||
constants.EnvKeyOrganizationName: "Authorizer",
|
constants.EnvKeyOrganizationName: "Authorizer",
|
||||||
constants.EnvKeyOrganizationLogo: "https://www.authorizer.io/images/logo.png",
|
constants.EnvKeyOrganizationLogo: "https://www.authorizer.dev/images/logo.png",
|
||||||
},
|
},
|
||||||
BoolEnv: map[string]bool{
|
BoolEnv: map[string]bool{
|
||||||
constants.EnvKeyDisableBasicAuthentication: false,
|
constants.EnvKeyDisableBasicAuthentication: false,
|
||||||
|
@@ -6,7 +6,6 @@ require (
|
|||||||
github.com/99designs/gqlgen v0.14.0
|
github.com/99designs/gqlgen v0.14.0
|
||||||
github.com/arangodb/go-driver v1.2.1
|
github.com/arangodb/go-driver v1.2.1
|
||||||
github.com/coreos/go-oidc/v3 v3.1.0
|
github.com/coreos/go-oidc/v3 v3.1.0
|
||||||
github.com/gin-contrib/location v0.0.2
|
|
||||||
github.com/gin-gonic/gin v1.7.2
|
github.com/gin-gonic/gin v1.7.2
|
||||||
github.com/go-playground/validator/v10 v10.8.0 // indirect
|
github.com/go-playground/validator/v10 v10.8.0 // indirect
|
||||||
github.com/go-redis/redis/v8 v8.11.0
|
github.com/go-redis/redis/v8 v8.11.0
|
||||||
|
@@ -82,11 +82,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
|
|||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4=
|
|
||||||
github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc=
|
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
|
||||||
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
|
||||||
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
@@ -100,7 +97,6 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
|
|||||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
|
||||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||||
github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k=
|
github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k=
|
||||||
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||||
|
@@ -35,7 +35,6 @@ type Env struct {
|
|||||||
JwtType *string `json:"JWT_TYPE"`
|
JwtType *string `json:"JWT_TYPE"`
|
||||||
JwtSecret *string `json:"JWT_SECRET"`
|
JwtSecret *string `json:"JWT_SECRET"`
|
||||||
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
|
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
|
||||||
AuthorizerURL *string `json:"AUTHORIZER_URL"`
|
|
||||||
AppURL *string `json:"APP_URL"`
|
AppURL *string `json:"APP_URL"`
|
||||||
RedisURL *string `json:"REDIS_URL"`
|
RedisURL *string `json:"REDIS_URL"`
|
||||||
CookieName *string `json:"COOKIE_NAME"`
|
CookieName *string `json:"COOKIE_NAME"`
|
||||||
@@ -93,6 +92,22 @@ type Meta struct {
|
|||||||
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
IsMagicLinkLoginEnabled bool `json:"is_magic_link_login_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PaginatedInput struct {
|
||||||
|
Pagination *PaginationInput `json:"pagination"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
Limit int64 `json:"limit"`
|
||||||
|
Page int64 `json:"page"`
|
||||||
|
Offset int64 `json:"offset"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationInput struct {
|
||||||
|
Limit *int64 `json:"limit"`
|
||||||
|
Page *int64 `json:"page"`
|
||||||
|
}
|
||||||
|
|
||||||
type ResendVerifyEmailInput struct {
|
type ResendVerifyEmailInput struct {
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
@@ -133,12 +148,12 @@ type UpdateEnvInput struct {
|
|||||||
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
|
OldAdminSecret *string `json:"OLD_ADMIN_SECRET"`
|
||||||
SMTPHost *string `json:"SMTP_HOST"`
|
SMTPHost *string `json:"SMTP_HOST"`
|
||||||
SMTPPort *string `json:"SMTP_PORT"`
|
SMTPPort *string `json:"SMTP_PORT"`
|
||||||
|
SMTPUsername *string `json:"SMTP_USERNAME"`
|
||||||
|
SMTPPassword *string `json:"SMTP_PASSWORD"`
|
||||||
SenderEmail *string `json:"SENDER_EMAIL"`
|
SenderEmail *string `json:"SENDER_EMAIL"`
|
||||||
SenderPassword *string `json:"SENDER_PASSWORD"`
|
|
||||||
JwtType *string `json:"JWT_TYPE"`
|
JwtType *string `json:"JWT_TYPE"`
|
||||||
JwtSecret *string `json:"JWT_SECRET"`
|
JwtSecret *string `json:"JWT_SECRET"`
|
||||||
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
|
AllowedOrigins []string `json:"ALLOWED_ORIGINS"`
|
||||||
AuthorizerURL *string `json:"AUTHORIZER_URL"`
|
|
||||||
AppURL *string `json:"APP_URL"`
|
AppURL *string `json:"APP_URL"`
|
||||||
RedisURL *string `json:"REDIS_URL"`
|
RedisURL *string `json:"REDIS_URL"`
|
||||||
CookieName *string `json:"COOKIE_NAME"`
|
CookieName *string `json:"COOKIE_NAME"`
|
||||||
@@ -211,6 +226,11 @@ type User struct {
|
|||||||
UpdatedAt *int64 `json:"updated_at"`
|
UpdatedAt *int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Users struct {
|
||||||
|
Pagination *Pagination `json:"pagination"`
|
||||||
|
Users []*User `json:"users"`
|
||||||
|
}
|
||||||
|
|
||||||
type ValidJWTResponse struct {
|
type ValidJWTResponse struct {
|
||||||
Valid bool `json:"valid"`
|
Valid bool `json:"valid"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -226,6 +246,11 @@ type VerificationRequest struct {
|
|||||||
UpdatedAt *int64 `json:"updated_at"`
|
UpdatedAt *int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VerificationRequests struct {
|
||||||
|
Pagination *Pagination `json:"pagination"`
|
||||||
|
VerificationRequests []*VerificationRequest `json:"verification_requests"`
|
||||||
|
}
|
||||||
|
|
||||||
type VerifyEmailInput struct {
|
type VerifyEmailInput struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,13 @@ scalar Int64
|
|||||||
scalar Map
|
scalar Map
|
||||||
scalar Any
|
scalar Any
|
||||||
|
|
||||||
|
type Pagination {
|
||||||
|
limit: Int64!
|
||||||
|
page: Int64!
|
||||||
|
offset: Int64!
|
||||||
|
total: Int64!
|
||||||
|
}
|
||||||
|
|
||||||
type Meta {
|
type Meta {
|
||||||
version: String!
|
version: String!
|
||||||
is_google_login_enabled: Boolean!
|
is_google_login_enabled: Boolean!
|
||||||
@@ -36,6 +43,11 @@ type User {
|
|||||||
updated_at: Int64
|
updated_at: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Users {
|
||||||
|
pagination: Pagination!
|
||||||
|
users: [User!]!
|
||||||
|
}
|
||||||
|
|
||||||
type VerificationRequest {
|
type VerificationRequest {
|
||||||
id: ID!
|
id: ID!
|
||||||
identifier: String
|
identifier: String
|
||||||
@@ -46,6 +58,11 @@ type VerificationRequest {
|
|||||||
updated_at: Int64
|
updated_at: Int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VerificationRequests {
|
||||||
|
pagination: Pagination!
|
||||||
|
verification_requests: [VerificationRequest!]!
|
||||||
|
}
|
||||||
|
|
||||||
type Error {
|
type Error {
|
||||||
message: String!
|
message: String!
|
||||||
reason: String!
|
reason: String!
|
||||||
@@ -81,7 +98,6 @@ type Env {
|
|||||||
JWT_TYPE: String
|
JWT_TYPE: String
|
||||||
JWT_SECRET: String
|
JWT_SECRET: String
|
||||||
ALLOWED_ORIGINS: [String!]
|
ALLOWED_ORIGINS: [String!]
|
||||||
AUTHORIZER_URL: String
|
|
||||||
APP_URL: String
|
APP_URL: String
|
||||||
REDIS_URL: String
|
REDIS_URL: String
|
||||||
COOKIE_NAME: String
|
COOKIE_NAME: String
|
||||||
@@ -110,12 +126,12 @@ input UpdateEnvInput {
|
|||||||
OLD_ADMIN_SECRET: String
|
OLD_ADMIN_SECRET: String
|
||||||
SMTP_HOST: String
|
SMTP_HOST: String
|
||||||
SMTP_PORT: String
|
SMTP_PORT: String
|
||||||
|
SMTP_USERNAME: String
|
||||||
|
SMTP_PASSWORD: String
|
||||||
SENDER_EMAIL: String
|
SENDER_EMAIL: String
|
||||||
SENDER_PASSWORD: String
|
|
||||||
JWT_TYPE: String
|
JWT_TYPE: String
|
||||||
JWT_SECRET: String
|
JWT_SECRET: String
|
||||||
ALLOWED_ORIGINS: [String!]
|
ALLOWED_ORIGINS: [String!]
|
||||||
AUTHORIZER_URL: String
|
|
||||||
APP_URL: String
|
APP_URL: String
|
||||||
REDIS_URL: String
|
REDIS_URL: String
|
||||||
COOKIE_NAME: String
|
COOKIE_NAME: String
|
||||||
@@ -234,6 +250,15 @@ input IsValidJWTQueryInput {
|
|||||||
roles: [String!]
|
roles: [String!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input PaginationInput {
|
||||||
|
limit: Int64
|
||||||
|
page: Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
input PaginatedInput {
|
||||||
|
pagination: PaginationInput
|
||||||
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
signup(params: SignUpInput!): AuthResponse!
|
signup(params: SignUpInput!): AuthResponse!
|
||||||
login(params: LoginInput!): AuthResponse!
|
login(params: LoginInput!): AuthResponse!
|
||||||
@@ -259,8 +284,8 @@ type Query {
|
|||||||
is_valid_jwt(params: IsValidJWTQueryInput): ValidJWTResponse!
|
is_valid_jwt(params: IsValidJWTQueryInput): ValidJWTResponse!
|
||||||
profile: User!
|
profile: User!
|
||||||
# admin only apis
|
# admin only apis
|
||||||
_users: [User!]!
|
_users(params: PaginatedInput): Users!
|
||||||
_verification_requests: [VerificationRequest!]!
|
_verification_requests(params: PaginatedInput): VerificationRequests!
|
||||||
_admin_session: Response!
|
_admin_session: Response!
|
||||||
_env: Env!
|
_env: Env!
|
||||||
}
|
}
|
||||||
|
@@ -87,12 +87,12 @@ func (r *queryResolver) Profile(ctx context.Context) (*model.User, error) {
|
|||||||
return resolvers.ProfileResolver(ctx)
|
return resolvers.ProfileResolver(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Users(ctx context.Context) ([]*model.User, error) {
|
func (r *queryResolver) Users(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
|
||||||
return resolvers.UsersResolver(ctx)
|
return resolvers.UsersResolver(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) VerificationRequests(ctx context.Context) ([]*model.VerificationRequest, error) {
|
func (r *queryResolver) VerificationRequests(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) {
|
||||||
return resolvers.VerificationRequestsResolver(ctx)
|
return resolvers.VerificationRequestsResolver(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) AdminSession(ctx context.Context) (*model.Response, error) {
|
func (r *queryResolver) AdminSession(ctx context.Context) (*model.Response, error) {
|
||||||
|
@@ -22,14 +22,19 @@ type State struct {
|
|||||||
// AppHandler is the handler for the /app route
|
// AppHandler is the handler for the /app route
|
||||||
func AppHandler() gin.HandlerFunc {
|
func AppHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
hostname := utils.GetHost(c)
|
||||||
|
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableLoginPage) {
|
||||||
|
c.JSON(400, gin.H{"error": "login page is not enabled"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
state := c.Query("state")
|
state := c.Query("state")
|
||||||
|
|
||||||
var stateObj State
|
var stateObj State
|
||||||
|
|
||||||
if state == "" {
|
if state == "" {
|
||||||
stateObj.AuthorizerURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL)
|
stateObj.AuthorizerURL = hostname
|
||||||
stateObj.RedirectURL = stateObj.AuthorizerURL + "/app"
|
stateObj.RedirectURL = hostname + "/app"
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
decodedState, err := utils.DecryptB64(state)
|
decodedState, err := utils.DecryptB64(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -57,7 +62,7 @@ func AppHandler() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate host and domain of authorizer url
|
// validate host and domain of authorizer url
|
||||||
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) {
|
if strings.TrimSuffix(stateObj.AuthorizerURL, "/") != hostname {
|
||||||
c.JSON(400, gin.H{"error": "invalid host url"})
|
c.JSON(400, gin.H{"error": "invalid host url"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@@ -99,6 +99,11 @@ func OAuthCallbackHandler() gin.HandlerFunc {
|
|||||||
user.SignupMethods = signupMethod
|
user.SignupMethods = signupMethod
|
||||||
user.Password = existingUser.Password
|
user.Password = existingUser.Password
|
||||||
|
|
||||||
|
if user.EmailVerifiedAt == nil {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
user.EmailVerifiedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
// There multiple scenarios with roles here in social login
|
// There multiple scenarios with roles here in social login
|
||||||
// 1. user has access to protected roles + roles and trying to login
|
// 1. user has access to protected roles + roles and trying to login
|
||||||
// 2. user has not signed up for one of the available role but trying to signup.
|
// 2. user has not signed up for one of the available role but trying to signup.
|
||||||
|
@@ -16,7 +16,7 @@ import (
|
|||||||
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
|
// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
|
||||||
func OAuthLoginHandler() gin.HandlerFunc {
|
func OAuthLoginHandler() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
// TODO validate redirect URL
|
hostname := utils.GetHost(c)
|
||||||
redirectURL := c.Query("redirectURL")
|
redirectURL := c.Query("redirectURL")
|
||||||
roles := c.Query("roles")
|
roles := c.Query("roles")
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGoogle)
|
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGoogle)
|
||||||
// during the init of OAuthProvider authorizer url might be empty
|
// during the init of OAuthProvider authorizer url might be empty
|
||||||
oauth.OAuthProviders.GoogleConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/google"
|
oauth.OAuthProviders.GoogleConfig.RedirectURL = hostname + "/oauth_callback/google"
|
||||||
url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
|
url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
case constants.SignupMethodGithub:
|
case constants.SignupMethodGithub:
|
||||||
@@ -65,7 +65,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGithub)
|
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodGithub)
|
||||||
oauth.OAuthProviders.GithubConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/github"
|
oauth.OAuthProviders.GithubConfig.RedirectURL = hostname + "/oauth_callback/github"
|
||||||
url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
|
url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
case constants.SignupMethodFacebook:
|
case constants.SignupMethodFacebook:
|
||||||
@@ -74,7 +74,7 @@ func OAuthLoginHandler() gin.HandlerFunc {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodFacebook)
|
sessionstore.SetSocailLoginState(oauthStateString, constants.SignupMethodFacebook)
|
||||||
oauth.OAuthProviders.FacebookConfig.RedirectURL = envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/facebook"
|
oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/facebook"
|
||||||
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
|
url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
|
||||||
c.Redirect(http.StatusTemporaryRedirect, url)
|
c.Redirect(http.StatusTemporaryRedirect, url)
|
||||||
default:
|
default:
|
||||||
|
@@ -3,19 +3,12 @@ package middlewares
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
|
||||||
"github.com/gin-contrib/location"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GinContextToContextMiddleware is a middleware to add gin context in context
|
// GinContextToContextMiddleware is a middleware to add gin context in context
|
||||||
func GinContextToContextMiddleware() gin.HandlerFunc {
|
func GinContextToContextMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
if envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) == "" {
|
|
||||||
url := location.Get(c)
|
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAuthorizerURL, url.Scheme+"://"+c.Request.Host)
|
|
||||||
}
|
|
||||||
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
|
ctx := context.WithValue(c.Request.Context(), "GinContextKey", c)
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
c.Next()
|
c.Next()
|
||||||
|
@@ -43,7 +43,7 @@ func InitOAuth() {
|
|||||||
OAuthProviders.GoogleConfig = &oauth2.Config{
|
OAuthProviders.GoogleConfig = &oauth2.Config{
|
||||||
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID),
|
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientID),
|
||||||
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret),
|
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGoogleClientSecret),
|
||||||
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/google",
|
RedirectURL: "/oauth_callback/google",
|
||||||
Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
|
Endpoint: OIDCProviders.GoogleOIDC.Endpoint(),
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func InitOAuth() {
|
|||||||
OAuthProviders.GithubConfig = &oauth2.Config{
|
OAuthProviders.GithubConfig = &oauth2.Config{
|
||||||
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID),
|
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientID),
|
||||||
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret),
|
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyGithubClientSecret),
|
||||||
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/github",
|
RedirectURL: "/oauth_callback/github",
|
||||||
Endpoint: githubOAuth2.Endpoint,
|
Endpoint: githubOAuth2.Endpoint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ func InitOAuth() {
|
|||||||
OAuthProviders.FacebookConfig = &oauth2.Config{
|
OAuthProviders.FacebookConfig = &oauth2.Config{
|
||||||
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientID),
|
ClientID: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientID),
|
||||||
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientSecret),
|
ClientSecret: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyFacebookClientSecret),
|
||||||
RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL) + "/oauth_callback/facebook",
|
RedirectURL: "/oauth_callback/facebook",
|
||||||
Endpoint: facebookOAuth2.Endpoint,
|
Endpoint: facebookOAuth2.Endpoint,
|
||||||
Scopes: []string{"public_profile", "email"},
|
Scopes: []string{"public_profile", "email"},
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,6 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
|||||||
jwtSecret := store.StringEnv[constants.EnvKeyJwtSecret]
|
jwtSecret := store.StringEnv[constants.EnvKeyJwtSecret]
|
||||||
jwtRoleClaim := store.StringEnv[constants.EnvKeyJwtRoleClaim]
|
jwtRoleClaim := store.StringEnv[constants.EnvKeyJwtRoleClaim]
|
||||||
allowedOrigins := store.SliceEnv[constants.EnvKeyAllowedOrigins]
|
allowedOrigins := store.SliceEnv[constants.EnvKeyAllowedOrigins]
|
||||||
authorizerURL := store.StringEnv[constants.EnvKeyAuthorizerURL]
|
|
||||||
appURL := store.StringEnv[constants.EnvKeyAppURL]
|
appURL := store.StringEnv[constants.EnvKeyAppURL]
|
||||||
redisURL := store.StringEnv[constants.EnvKeyRedisURL]
|
redisURL := store.StringEnv[constants.EnvKeyRedisURL]
|
||||||
cookieName := store.StringEnv[constants.EnvKeyCookieName]
|
cookieName := store.StringEnv[constants.EnvKeyCookieName]
|
||||||
@@ -77,7 +76,6 @@ func EnvResolver(ctx context.Context) (*model.Env, error) {
|
|||||||
JwtSecret: &jwtSecret,
|
JwtSecret: &jwtSecret,
|
||||||
JwtRoleClaim: &jwtRoleClaim,
|
JwtRoleClaim: &jwtRoleClaim,
|
||||||
AllowedOrigins: allowedOrigins,
|
AllowedOrigins: allowedOrigins,
|
||||||
AuthorizerURL: &authorizerURL,
|
|
||||||
AppURL: &appURL,
|
AppURL: &appURL,
|
||||||
RedisURL: &redisURL,
|
RedisURL: &redisURL,
|
||||||
CookieName: &cookieName,
|
CookieName: &cookieName,
|
||||||
|
@@ -27,7 +27,6 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
|||||||
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableBasicAuthentication) {
|
||||||
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
return res, fmt.Errorf(`basic authentication is disabled for this instance`)
|
||||||
}
|
}
|
||||||
host := gc.Request.Host
|
|
||||||
params.Email = strings.ToLower(params.Email)
|
params.Email = strings.ToLower(params.Email)
|
||||||
|
|
||||||
if !utils.IsValidEmail(params.Email) {
|
if !utils.IsValidEmail(params.Email) {
|
||||||
@@ -39,7 +38,8 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
|||||||
return res, fmt.Errorf(`user with this email not found`)
|
return res, fmt.Errorf(`user with this email not found`)
|
||||||
}
|
}
|
||||||
|
|
||||||
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword)
|
hostname := utils.GetHost(gc)
|
||||||
|
verificationToken, err := token.CreateVerificationToken(params.Email, constants.VerificationTypeForgotPassword, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func ForgotPasswordResolver(ctx context.Context, params model.ForgotPasswordInpu
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendForgotPasswordMail(params.Email, verificationToken, host)
|
email.SendForgotPasswordMail(params.Email, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
res = &model.Response{
|
res = &model.Response{
|
||||||
|
@@ -20,6 +20,10 @@ import (
|
|||||||
// MagicLinkLoginResolver is a resolver for magic link login mutation
|
// MagicLinkLoginResolver is a resolver for magic link login mutation
|
||||||
func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
|
func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInput) (*model.Response, error) {
|
||||||
var res *model.Response
|
var res *model.Response
|
||||||
|
gc, err := utils.GinContextFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
if envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableMagicLinkLogin) {
|
||||||
return res, fmt.Errorf(`magic link login is disabled for this instance`)
|
return res, fmt.Errorf(`magic link login is disabled for this instance`)
|
||||||
@@ -102,10 +106,11 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||||
// insert verification request
|
// insert verification request
|
||||||
verificationType := constants.VerificationTypeMagicLinkLogin
|
verificationType := constants.VerificationTypeMagicLinkLogin
|
||||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType)
|
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -118,7 +123,7 @@ func MagicLinkLoginResolver(ctx context.Context, params model.MagicLinkLoginInpu
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendVerificationMail(params.Email, verificationToken)
|
email.SendVerificationMail(params.Email, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -18,6 +18,10 @@ import (
|
|||||||
// ResendVerifyEmailResolver is a resolver for resend verify email mutation
|
// ResendVerifyEmailResolver is a resolver for resend verify email mutation
|
||||||
func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) {
|
func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEmailInput) (*model.Response, error) {
|
||||||
var res *model.Response
|
var res *model.Response
|
||||||
|
gc, err := utils.GinContextFromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
params.Email = strings.ToLower(params.Email)
|
params.Email = strings.ToLower(params.Email)
|
||||||
|
|
||||||
if !utils.IsValidEmail(params.Email) {
|
if !utils.IsValidEmail(params.Email) {
|
||||||
@@ -39,7 +43,8 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
|
|||||||
log.Println("error deleting verification request:", err)
|
log.Println("error deleting verification request:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier)
|
hostname := utils.GetHost(gc)
|
||||||
|
verificationToken, err := token.CreateVerificationToken(params.Email, params.Identifier, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -52,7 +57,7 @@ func ResendVerifyEmailResolver(ctx context.Context, params model.ResendVerifyEma
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendVerificationMail(params.Email, verificationToken)
|
email.SendVerificationMail(params.Email, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
res = &model.Response{
|
res = &model.Response{
|
||||||
|
@@ -119,10 +119,11 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
|||||||
roles := strings.Split(user.Roles, ",")
|
roles := strings.Split(user.Roles, ",")
|
||||||
userToReturn := user.AsAPIUser()
|
userToReturn := user.AsAPIUser()
|
||||||
|
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableEmailVerification) {
|
||||||
// insert verification request
|
// insert verification request
|
||||||
verificationType := constants.VerificationTypeBasicAuthSignup
|
verificationType := constants.VerificationTypeBasicAuthSignup
|
||||||
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType)
|
verificationToken, err := token.CreateVerificationToken(params.Email, verificationType, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -135,7 +136,7 @@ func SignupResolver(ctx context.Context, params model.SignUpInput) (*model.AuthR
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendVerificationMail(params.Email, verificationToken)
|
email.SendVerificationMail(params.Email, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
res = &model.AuthResponse{
|
res = &model.AuthResponse{
|
||||||
|
@@ -13,9 +13,10 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/db"
|
"github.com/authorizerdev/authorizer/server/db"
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
"github.com/authorizerdev/authorizer/server/oauth"
|
||||||
|
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||||
"github.com/authorizerdev/authorizer/server/token"
|
"github.com/authorizerdev/authorizer/server/token"
|
||||||
"github.com/authorizerdev/authorizer/server/utils"
|
"github.com/authorizerdev/authorizer/server/utils"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateEnvResolver is a resolver for update config mutation
|
// UpdateEnvResolver is a resolver for update config mutation
|
||||||
@@ -43,6 +44,23 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
|||||||
return res, fmt.Errorf("error un-marshalling params: %t", err)
|
return res, fmt.Errorf("error un-marshalling params: %t", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// in case of admin secret change update the cookie with new hash
|
||||||
|
if params.AdminSecret != nil {
|
||||||
|
if params.OldAdminSecret == nil {
|
||||||
|
return res, errors.New("admin secret and old admin secret are required for secret change")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *params.OldAdminSecret != envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret) {
|
||||||
|
return res, errors.New("old admin secret is not correct")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*params.AdminSecret) < 6 {
|
||||||
|
err = fmt.Errorf("admin secret must be at least 6 characters")
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
updatedData := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
updatedData := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
||||||
for key, value := range data {
|
for key, value := range data {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
@@ -99,6 +117,8 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
|||||||
|
|
||||||
// Update local store
|
// Update local store
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvStore(updatedData)
|
envstore.EnvInMemoryStoreObj.UpdateEnvStore(updatedData)
|
||||||
|
sessionstore.InitSession()
|
||||||
|
oauth.InitOAuth()
|
||||||
|
|
||||||
// Fetch the current db store and update it
|
// Fetch the current db store and update it
|
||||||
env, err := db.Provider.GetEnv()
|
env, err := db.Provider.GetEnv()
|
||||||
@@ -106,22 +126,7 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
encryptedConfig, err := utils.EncryptEnvData(updatedData)
|
|
||||||
if err != nil {
|
|
||||||
return res, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// in case of admin secret change update the cookie with new hash
|
|
||||||
if params.AdminSecret != nil {
|
if params.AdminSecret != nil {
|
||||||
if params.OldAdminSecret == nil {
|
|
||||||
return res, errors.New("admin secret and old admin secret are required for secret change")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(*params.OldAdminSecret), []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)))
|
|
||||||
if err != nil {
|
|
||||||
return res, errors.New("old admin secret is not correct")
|
|
||||||
}
|
|
||||||
|
|
||||||
hashedKey, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
hashedKey, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
@@ -129,6 +134,11 @@ func UpdateEnvResolver(ctx context.Context, params model.UpdateEnvInput) (*model
|
|||||||
cookie.SetAdminCookie(gc, hashedKey)
|
cookie.SetAdminCookie(gc, hashedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encryptedConfig, err := utils.EncryptEnvData(updatedData)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
env.EnvData = encryptedConfig
|
env.EnvData = encryptedConfig
|
||||||
_, err = db.Provider.UpdateEnv(env)
|
_, err = db.Provider.UpdateEnv(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -116,12 +116,13 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
|||||||
sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
|
sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
|
||||||
cookie.DeleteCookie(gc)
|
cookie.DeleteCookie(gc)
|
||||||
|
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
user.Email = newEmail
|
user.Email = newEmail
|
||||||
user.EmailVerifiedAt = nil
|
user.EmailVerifiedAt = nil
|
||||||
hasEmailChanged = true
|
hasEmailChanged = true
|
||||||
// insert verification request
|
// insert verification request
|
||||||
verificationType := constants.VerificationTypeUpdateEmail
|
verificationType := constants.VerificationTypeUpdateEmail
|
||||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType)
|
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,7 @@ func UpdateProfileResolver(ctx context.Context, params model.UpdateProfileInput)
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendVerificationMail(newEmail, verificationToken)
|
email.SendVerificationMail(newEmail, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -98,11 +98,12 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
|||||||
sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
|
sessionstore.DeleteAllUserSession(fmt.Sprintf("%v", user.ID))
|
||||||
cookie.DeleteCookie(gc)
|
cookie.DeleteCookie(gc)
|
||||||
|
|
||||||
|
hostname := utils.GetHost(gc)
|
||||||
user.Email = newEmail
|
user.Email = newEmail
|
||||||
user.EmailVerifiedAt = nil
|
user.EmailVerifiedAt = nil
|
||||||
// insert verification request
|
// insert verification request
|
||||||
verificationType := constants.VerificationTypeUpdateEmail
|
verificationType := constants.VerificationTypeUpdateEmail
|
||||||
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType)
|
verificationToken, err := token.CreateVerificationToken(newEmail, verificationType, hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(`error generating token`, err)
|
log.Println(`error generating token`, err)
|
||||||
}
|
}
|
||||||
@@ -115,7 +116,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
|||||||
|
|
||||||
// exec it as go routin so that we can reduce the api latency
|
// exec it as go routin so that we can reduce the api latency
|
||||||
go func() {
|
go func() {
|
||||||
email.SendVerificationMail(newEmail, verificationToken)
|
email.SendVerificationMail(newEmail, verificationToken, hostname)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ func UpdateUserResolver(ctx context.Context, params model.UpdateUserInput) (*mod
|
|||||||
inputRoles = append(inputRoles, *item)
|
inputRoles = append(inputRoles, *item)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utils.IsValidRoles(append([]string{}, append(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyRoles), envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyProtectedRoles)...)...), inputRoles) {
|
if !utils.IsValidRoles(inputRoles, append([]string{}, append(envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyRoles), envstore.EnvInMemoryStoreObj.GetSliceStoreEnvVariable(constants.EnvKeyProtectedRoles)...)...)) {
|
||||||
return res, fmt.Errorf("invalid list of roles")
|
return res, fmt.Errorf("invalid list of roles")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,24 +12,21 @@ import (
|
|||||||
|
|
||||||
// UsersResolver is a resolver for users query
|
// UsersResolver is a resolver for users query
|
||||||
// This is admin only query
|
// This is admin only query
|
||||||
func UsersResolver(ctx context.Context) ([]*model.User, error) {
|
func UsersResolver(ctx context.Context, params *model.PaginatedInput) (*model.Users, error) {
|
||||||
gc, err := utils.GinContextFromContext(ctx)
|
gc, err := utils.GinContextFromContext(ctx)
|
||||||
var res []*model.User
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.IsSuperAdmin(gc) {
|
if !token.IsSuperAdmin(gc) {
|
||||||
return res, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := db.Provider.ListUsers()
|
pagination := utils.GetPagination(params)
|
||||||
|
|
||||||
|
res, err := db.Provider.ListUsers(pagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(users); i++ {
|
|
||||||
res = append(res, users[i].AsAPIUser())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@@ -12,32 +12,21 @@ import (
|
|||||||
|
|
||||||
// VerificationRequestsResolver is a resolver for verification requests query
|
// VerificationRequestsResolver is a resolver for verification requests query
|
||||||
// This is admin only query
|
// This is admin only query
|
||||||
func VerificationRequestsResolver(ctx context.Context) ([]*model.VerificationRequest, error) {
|
func VerificationRequestsResolver(ctx context.Context, params *model.PaginatedInput) (*model.VerificationRequests, error) {
|
||||||
gc, err := utils.GinContextFromContext(ctx)
|
gc, err := utils.GinContextFromContext(ctx)
|
||||||
var res []*model.VerificationRequest
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !token.IsSuperAdmin(gc) {
|
if !token.IsSuperAdmin(gc) {
|
||||||
return res, fmt.Errorf("unauthorized")
|
return nil, fmt.Errorf("unauthorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
verificationRequests, err := db.Provider.ListVerificationRequests()
|
pagination := utils.GetPagination(params)
|
||||||
|
|
||||||
|
res, err := db.Provider.ListVerificationRequests(pagination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(verificationRequests); i++ {
|
|
||||||
res = append(res, &model.VerificationRequest{
|
|
||||||
ID: fmt.Sprintf("%v", verificationRequests[i].ID),
|
|
||||||
Email: &verificationRequests[i].Email,
|
|
||||||
Token: &verificationRequests[i].Token,
|
|
||||||
Identifier: &verificationRequests[i].Identifier,
|
|
||||||
Expires: &verificationRequests[i].ExpiresAt,
|
|
||||||
CreatedAt: &verificationRequests[i].CreatedAt,
|
|
||||||
UpdatedAt: &verificationRequests[i].UpdatedAt,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
|
@@ -1,18 +1,15 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
|
||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
|
||||||
"github.com/authorizerdev/authorizer/server/handlers"
|
"github.com/authorizerdev/authorizer/server/handlers"
|
||||||
"github.com/authorizerdev/authorizer/server/middlewares"
|
"github.com/authorizerdev/authorizer/server/middlewares"
|
||||||
"github.com/gin-contrib/location"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitRouter initializes gin router
|
// InitRouter initializes gin router
|
||||||
func InitRouter() *gin.Engine {
|
func InitRouter() *gin.Engine {
|
||||||
router := gin.Default()
|
router := gin.Default()
|
||||||
router.Use(location.Default())
|
// router.Use(location.Default())
|
||||||
router.Use(middlewares.GinContextToContextMiddleware())
|
router.Use(middlewares.GinContextToContextMiddleware())
|
||||||
router.Use(middlewares.CORSMiddleware())
|
router.Use(middlewares.CORSMiddleware())
|
||||||
|
|
||||||
@@ -25,21 +22,21 @@ func InitRouter() *gin.Engine {
|
|||||||
|
|
||||||
router.LoadHTMLGlob("templates/*")
|
router.LoadHTMLGlob("templates/*")
|
||||||
// login page app related routes.
|
// login page app related routes.
|
||||||
if !envstore.EnvInMemoryStoreObj.GetBoolStoreEnvVariable(constants.EnvKeyDisableLoginPage) {
|
|
||||||
app := router.Group("/app")
|
app := router.Group("/app")
|
||||||
{
|
{
|
||||||
|
app.Static("/favicon_io", "app/favicon_io")
|
||||||
app.Static("/build", "app/build")
|
app.Static("/build", "app/build")
|
||||||
app.GET("/", handlers.AppHandler())
|
app.GET("/", handlers.AppHandler())
|
||||||
app.GET("/reset-password", handlers.AppHandler())
|
app.GET("/:page", handlers.AppHandler())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dashboard related routes
|
// dashboard related routes
|
||||||
app := router.Group("/dashboard")
|
dashboard := router.Group("/dashboard")
|
||||||
{
|
{
|
||||||
app.Static("/build", "dashboard/build")
|
dashboard.Static("/favicon_io", "dashboard/favicon_io")
|
||||||
app.GET("/", handlers.DashboardHandler())
|
dashboard.Static("/build", "dashboard/build")
|
||||||
app.GET("/:page", handlers.DashboardHandler())
|
dashboard.GET("/", handlers.DashboardHandler())
|
||||||
|
dashboard.GET("/:page", handlers.DashboardHandler())
|
||||||
}
|
}
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
@@ -7,13 +7,12 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/envstore"
|
"github.com/authorizerdev/authorizer/server/envstore"
|
||||||
"github.com/authorizerdev/authorizer/server/graph/model"
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
"github.com/authorizerdev/authorizer/server/resolvers"
|
"github.com/authorizerdev/authorizer/server/resolvers"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func adminSignupTests(t *testing.T, s TestSetup) {
|
func adminSignupTests(t *testing.T, s TestSetup) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
t.Run(`should complete admin login`, func(t *testing.T) {
|
t.Run(`should complete admin signup`, func(t *testing.T) {
|
||||||
_, ctx := createContext(s)
|
_, ctx := createContext(s)
|
||||||
_, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
|
_, err := resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
|
||||||
AdminSecret: "admin",
|
AdminSecret: "admin",
|
||||||
@@ -24,7 +23,7 @@ func adminSignupTests(t *testing.T, s TestSetup) {
|
|||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAdminSecret, "")
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyAdminSecret, "")
|
||||||
|
|
||||||
_, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
|
_, err = resolvers.AdminSignupResolver(ctx, model.AdminSignupInput{
|
||||||
AdminSecret: uuid.New().String(),
|
AdminSecret: "admin123",
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
@@ -14,16 +14,13 @@ func TestEnvs(t *testing.T) {
|
|||||||
env.InitEnv()
|
env.InitEnv()
|
||||||
store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
store := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
||||||
|
|
||||||
assert.Equal(t, store.StringEnv[constants.EnvKeyAdminSecret], "admin")
|
|
||||||
assert.Equal(t, store.StringEnv[constants.EnvKeyEnv], "production")
|
assert.Equal(t, store.StringEnv[constants.EnvKeyEnv], "production")
|
||||||
assert.False(t, store.BoolEnv[constants.EnvKeyDisableEmailVerification])
|
assert.False(t, store.BoolEnv[constants.EnvKeyDisableEmailVerification])
|
||||||
assert.False(t, store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin])
|
assert.False(t, store.BoolEnv[constants.EnvKeyDisableMagicLinkLogin])
|
||||||
assert.False(t, store.BoolEnv[constants.EnvKeyDisableBasicAuthentication])
|
assert.False(t, store.BoolEnv[constants.EnvKeyDisableBasicAuthentication])
|
||||||
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtType], "HS256")
|
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtType], "HS256")
|
||||||
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtSecret], "random_string")
|
|
||||||
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtRoleClaim], "role")
|
assert.Equal(t, store.StringEnv[constants.EnvKeyJwtRoleClaim], "role")
|
||||||
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyRoles], []string{"user"})
|
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyRoles], []string{"user"})
|
||||||
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyDefaultRoles], []string{"user"})
|
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyDefaultRoles], []string{"user"})
|
||||||
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyProtectedRoles], []string{"admin"})
|
|
||||||
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyAllowedOrigins], []string{"*"})
|
assert.EqualValues(t, store.SliceEnv[constants.EnvKeyAllowedOrigins], []string{"*"})
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
@@ -12,28 +13,28 @@ import (
|
|||||||
func TestResolvers(t *testing.T) {
|
func TestResolvers(t *testing.T) {
|
||||||
databases := map[string]string{
|
databases := map[string]string{
|
||||||
constants.DbTypeSqlite: "../../data.db",
|
constants.DbTypeSqlite: "../../data.db",
|
||||||
// constants.DbTypeArangodb: "http://localhost:8529",
|
constants.DbTypeArangodb: "http://localhost:8529",
|
||||||
// constants.DbTypeMongodb: "mongodb://localhost:27017",
|
constants.DbTypeMongodb: "mongodb://localhost:27017",
|
||||||
}
|
}
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, "test")
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyVersion, "test")
|
||||||
for dbType, dbURL := range databases {
|
for dbType, dbURL := range databases {
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseURL, dbURL)
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseType, dbType)
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyDatabaseType, dbType)
|
||||||
|
|
||||||
env.InitEnv()
|
s := testSetup()
|
||||||
|
defer s.Server.Close()
|
||||||
|
|
||||||
db.InitDB()
|
db.InitDB()
|
||||||
|
|
||||||
// clean the persisted config for test to use fresh config
|
// clean the persisted config for test to use fresh config
|
||||||
envData, err := db.Provider.GetEnv()
|
envData, err := db.Provider.GetEnv()
|
||||||
|
log.Println("=> envData:", envstore.EnvInMemoryStoreObj.GetEnvStoreClone())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
envData.EnvData = []byte{}
|
envData.EnvData = ""
|
||||||
db.Provider.UpdateEnv(envData)
|
db.Provider.UpdateEnv(envData)
|
||||||
}
|
}
|
||||||
env.PersistEnv()
|
env.PersistEnv()
|
||||||
|
|
||||||
s := testSetup()
|
|
||||||
defer s.Server.Close()
|
|
||||||
|
|
||||||
t.Run("should pass tests for "+dbType, func(t *testing.T) {
|
t.Run("should pass tests for "+dbType, func(t *testing.T) {
|
||||||
// admin tests
|
// admin tests
|
||||||
adminSignupTests(t, s)
|
adminSignupTests(t, s)
|
||||||
|
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/authorizerdev/authorizer/server/handlers"
|
"github.com/authorizerdev/authorizer/server/handlers"
|
||||||
"github.com/authorizerdev/authorizer/server/middlewares"
|
"github.com/authorizerdev/authorizer/server/middlewares"
|
||||||
"github.com/authorizerdev/authorizer/server/sessionstore"
|
"github.com/authorizerdev/authorizer/server/sessionstore"
|
||||||
"github.com/gin-contrib/location"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,13 +72,17 @@ func testSetup() TestSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, "../../.env.sample")
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeyEnvPath, "../../.env.sample")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySmtpHost, "smtp.yopmail.com")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySmtpPort, "2525")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySmtpUsername, "lakhan@yopmail.com")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySmtpPassword, "test")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.StringStoreIdentifier, constants.EnvKeySenderEmail, "info@yopmail.com")
|
||||||
|
envstore.EnvInMemoryStoreObj.UpdateEnvVariable(constants.SliceStoreIdentifier, constants.EnvKeyProtectedRoles, []string{"admin"})
|
||||||
env.InitEnv()
|
env.InitEnv()
|
||||||
sessionstore.InitSession()
|
sessionstore.InitSession()
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, r := gin.CreateTestContext(w)
|
c, r := gin.CreateTestContext(w)
|
||||||
r.Use(location.Default())
|
|
||||||
r.Use(middlewares.GinContextToContextMiddleware())
|
r.Use(middlewares.GinContextToContextMiddleware())
|
||||||
r.Use(middlewares.CORSMiddleware())
|
r.Use(middlewares.CORSMiddleware())
|
||||||
|
|
||||||
|
@@ -24,7 +24,7 @@ func updateUserTest(t *testing.T, s TestSetup) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
user := *signupRes.User
|
user := *signupRes.User
|
||||||
adminRole := "admin"
|
adminRole := "supplier"
|
||||||
userRole := "user"
|
userRole := "user"
|
||||||
newRoles := []*string{&adminRole, &userRole}
|
newRoles := []*string{&adminRole, &userRole}
|
||||||
_, err := resolvers.UpdateUserResolver(ctx, model.UpdateUserInput{
|
_, err := resolvers.UpdateUserResolver(ctx, model.UpdateUserInput{
|
||||||
|
@@ -8,9 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGetHostName(t *testing.T) {
|
func TestGetHostName(t *testing.T) {
|
||||||
authorizer_url := "http://test.herokuapp.com:80"
|
url := "http://test.herokuapp.com:80"
|
||||||
|
|
||||||
host, port := utils.GetHostParts(authorizer_url)
|
host, port := utils.GetHostParts(url)
|
||||||
expectedHost := "test.herokuapp.com"
|
expectedHost := "test.herokuapp.com"
|
||||||
|
|
||||||
assert.Equal(t, host, expectedHost, "hostname should be equal")
|
assert.Equal(t, host, expectedHost, "hostname should be equal")
|
||||||
@@ -18,9 +18,9 @@ func TestGetHostName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetDomainName(t *testing.T) {
|
func TestGetDomainName(t *testing.T) {
|
||||||
authorizer_url := "http://test.herokuapp.com"
|
url := "http://test.herokuapp.com"
|
||||||
|
|
||||||
got := utils.GetDomainName(authorizer_url)
|
got := utils.GetDomainName(url)
|
||||||
want := "herokuapp.com"
|
want := "herokuapp.com"
|
||||||
|
|
||||||
assert.Equal(t, got, want, "domain name should be equal")
|
assert.Equal(t, got, want, "domain name should be equal")
|
||||||
|
@@ -2,6 +2,7 @@ package test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
@@ -23,15 +24,26 @@ func usersTest(t *testing.T, s TestSetup) {
|
|||||||
ConfirmPassword: s.TestInfo.Password,
|
ConfirmPassword: s.TestInfo.Password,
|
||||||
})
|
})
|
||||||
|
|
||||||
users, err := resolvers.UsersResolver(ctx)
|
limit := int64(10)
|
||||||
|
page := int64(1)
|
||||||
|
pagination := &model.PaginatedInput{
|
||||||
|
Pagination: &model.PaginationInput{
|
||||||
|
Limit: &limit,
|
||||||
|
Page: &page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
usersRes, err := resolvers.UsersResolver(ctx, pagination)
|
||||||
assert.NotNil(t, err, "unauthorized")
|
assert.NotNil(t, err, "unauthorized")
|
||||||
|
|
||||||
h, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
h, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||||
users, err = resolvers.UsersResolver(ctx)
|
|
||||||
|
usersRes, err = resolvers.UsersResolver(ctx, pagination)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
rLen := len(users)
|
log.Println("=> userRes:", usersRes)
|
||||||
|
rLen := len(usersRes.Users)
|
||||||
assert.GreaterOrEqual(t, rLen, 1)
|
assert.GreaterOrEqual(t, rLen, 1)
|
||||||
|
|
||||||
cleanData(email)
|
cleanData(email)
|
||||||
|
@@ -25,16 +25,26 @@ func verificationRequestsTest(t *testing.T, s TestSetup) {
|
|||||||
ConfirmPassword: s.TestInfo.Password,
|
ConfirmPassword: s.TestInfo.Password,
|
||||||
})
|
})
|
||||||
|
|
||||||
requests, err := resolvers.VerificationRequestsResolver(ctx)
|
limit := int64(10)
|
||||||
|
page := int64(1)
|
||||||
|
pagination := &model.PaginatedInput{
|
||||||
|
Pagination: &model.PaginationInput{
|
||||||
|
Limit: &limit,
|
||||||
|
Page: &page,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
requests, err := resolvers.VerificationRequestsResolver(ctx, pagination)
|
||||||
assert.NotNil(t, err, "unauthorized")
|
assert.NotNil(t, err, "unauthorized")
|
||||||
|
|
||||||
h, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
h, err := utils.EncryptPassword(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret))
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminCookieName), h))
|
||||||
requests, err = resolvers.VerificationRequestsResolver(ctx)
|
requests, err = resolvers.VerificationRequestsResolver(ctx, pagination)
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
rLen := len(requests)
|
|
||||||
|
rLen := len(requests.VerificationRequests)
|
||||||
assert.GreaterOrEqual(t, rLen, 1)
|
assert.GreaterOrEqual(t, rLen, 1)
|
||||||
|
|
||||||
cleanData(email)
|
cleanData(email)
|
||||||
|
@@ -2,7 +2,6 @@ package token
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/authorizerdev/authorizer/server/constants"
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
"github.com/authorizerdev/authorizer/server/cookie"
|
"github.com/authorizerdev/authorizer/server/cookie"
|
||||||
@@ -25,7 +24,7 @@ func GetAdminAuthToken(gc *gin.Context) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(token), []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)))
|
err = bcrypt.CompareHashAndPassword([]byte(token), []byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAdminSecret)))
|
||||||
log.Println("error comparing hash:", err)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf(`unauthorized`)
|
return "", fmt.Errorf(`unauthorized`)
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ type CustomClaim struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateVerificationToken creates a verification JWT token
|
// CreateVerificationToken creates a verification JWT token
|
||||||
func CreateVerificationToken(email string, tokenType string) (string, error) {
|
func CreateVerificationToken(email, tokenType, hostname string) (string, error) {
|
||||||
t := jwt.New(jwt.GetSigningMethod(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)))
|
t := jwt.New(jwt.GetSigningMethod(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtType)))
|
||||||
|
|
||||||
t.Claims = &CustomClaim{
|
t.Claims = &CustomClaim{
|
||||||
@@ -31,7 +31,7 @@ func CreateVerificationToken(email string, tokenType string) (string, error) {
|
|||||||
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
ExpiresAt: time.Now().Add(time.Minute * 30).Unix(),
|
||||||
},
|
},
|
||||||
tokenType,
|
tokenType,
|
||||||
VerificationRequestToken{Email: email, Host: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAuthorizerURL), RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)},
|
VerificationRequestToken{Email: email, Host: hostname, RedirectURL: envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyAppURL)},
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.SignedString([]byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)))
|
return t.SignedString([]byte(envstore.EnvInMemoryStoreObj.GetStringStoreEnvVariable(constants.EnvKeyJwtSecret)))
|
||||||
|
@@ -34,3 +34,16 @@ func SaveSessionInDB(userId string, c *gin.Context) {
|
|||||||
log.Println("=> session saved in db:", sessionData)
|
log.Println("=> session saved in db:", sessionData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveDuplicateString removes duplicate strings from a string slice
|
||||||
|
func RemoveDuplicateString(strSlice []string) []string {
|
||||||
|
allKeys := make(map[string]bool)
|
||||||
|
list := []string{}
|
||||||
|
for _, item := range strSlice {
|
||||||
|
if _, value := allKeys[item]; !value {
|
||||||
|
allKeys[item] = true
|
||||||
|
list = append(list, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
@@ -90,29 +90,29 @@ func DecryptAES(ciphertext []byte) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EncryptEnvData is used to encrypt the env data
|
// EncryptEnvData is used to encrypt the env data
|
||||||
func EncryptEnvData(data envstore.Store) ([]byte, error) {
|
func EncryptEnvData(data envstore.Store) (string, error) {
|
||||||
jsonBytes, err := json.Marshal(data)
|
jsonBytes, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
envStoreObj := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
envStoreObj := envstore.EnvInMemoryStoreObj.GetEnvStoreClone()
|
||||||
|
|
||||||
err = json.Unmarshal(jsonBytes, &envStoreObj)
|
err = json.Unmarshal(jsonBytes, &envStoreObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
configData, err := json.Marshal(envStoreObj)
|
configData, err := json.Marshal(envStoreObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
encryptedConfig, err := EncryptAES(configData)
|
encryptedConfig, err := EncryptAES(configData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return encryptedConfig, nil
|
return EncryptB64(string(encryptedConfig)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptPassword is used for encrypting password
|
// EncryptPassword is used for encrypting password
|
||||||
|
29
server/utils/pagination.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/authorizerdev/authorizer/server/constants"
|
||||||
|
"github.com/authorizerdev/authorizer/server/graph/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetPagination helps getting pagination data from paginated input
|
||||||
|
// also returns default limit and offset if pagination data is not present
|
||||||
|
func GetPagination(paginatedInput *model.PaginatedInput) model.Pagination {
|
||||||
|
limit := int64(constants.DefaultLimit)
|
||||||
|
page := int64(1)
|
||||||
|
|
||||||
|
if paginatedInput != nil && paginatedInput.Pagination != nil {
|
||||||
|
if paginatedInput.Pagination.Limit != nil {
|
||||||
|
limit = *paginatedInput.Pagination.Limit
|
||||||
|
}
|
||||||
|
|
||||||
|
if paginatedInput.Pagination.Page != nil {
|
||||||
|
page = *paginatedInput.Pagination.Page
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Pagination{
|
||||||
|
Limit: limit,
|
||||||
|
Offset: (page - 1) * limit,
|
||||||
|
Page: page,
|
||||||
|
}
|
||||||
|
}
|
@@ -1,10 +1,24 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetHost returns hostname from request context
|
||||||
|
func GetHost(c *gin.Context) string {
|
||||||
|
scheme := c.Request.Header.Get("X-Forwarded-Proto")
|
||||||
|
if scheme != "https" {
|
||||||
|
scheme = "http"
|
||||||
|
}
|
||||||
|
host := c.Request.Host
|
||||||
|
log.Println("=> host:", scheme+"://"+host)
|
||||||
|
return scheme + "://" + host
|
||||||
|
}
|
||||||
|
|
||||||
// GetHostName function returns hostname and port
|
// GetHostName function returns hostname and port
|
||||||
func GetHostParts(uri string) (string, string) {
|
func GetHostParts(uri string) (string, string) {
|
||||||
tempURI := uri
|
tempURI := uri
|
||||||
|
@@ -2,8 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<title>{{.data.organizationName}}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<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">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<link rel="stylesheet" href="/app/build/index.css">
|
<link rel="stylesheet" href="/app/build/index.css">
|
||||||
<script>
|
<script>
|
||||||
|
@@ -2,8 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<title>Authorizer | Dashboard</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
<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="/dashboard/favicon_io/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/dashboard/favicon_io/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/dashboard/favicon_io/favicon-16x16.png">
|
||||||
<title>Document</title>
|
<title>Document</title>
|
||||||
<script>
|
<script>
|
||||||
window.__authorizer__ = {{.data}}
|
window.__authorizer__ = {{.data}}
|
||||||
|