-
Notifications
You must be signed in to change notification settings - Fork 988
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dc76be3
commit efd5016
Showing
4 changed files
with
419 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import { Fragment, useState } from "react"; | ||
import { useTranslation } from "next-i18next"; | ||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; | ||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||
import { Dialog, Transition } from "@headlessui/react"; | ||
|
||
import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; | ||
import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; | ||
|
||
import { envMapping } from "../../../public/data/frequentConstants"; | ||
import { | ||
decryptAssymmetric, | ||
encryptSymmetric, | ||
} from "../../utilities/cryptography/crypto"; | ||
import Button from "../buttons/Button"; | ||
import InputField from "../InputField"; | ||
import ListBox from "../Listbox"; | ||
|
||
const expiryMapping = { | ||
"1 day": 86400, | ||
"7 days": 604800, | ||
"1 month": 2592000, | ||
"6 months": 15552000, | ||
"12 months": 31104000, | ||
}; | ||
|
||
const crypto = require('crypto'); | ||
|
||
const AddApiKeyDialog = ({ | ||
isOpen, | ||
closeModal, | ||
workspaceId, | ||
workspaceName, | ||
serviceTokens, | ||
setServiceTokens | ||
}) => { | ||
const [serviceToken, setServiceToken] = useState(""); | ||
const [serviceTokenName, setServiceTokenName] = useState(""); | ||
const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); | ||
const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); | ||
const [serviceTokenCopied, setServiceTokenCopied] = useState(false); | ||
const { t } = useTranslation(); | ||
|
||
const generateServiceToken = async () => { | ||
const latestFileKey = await getLatestFileKey({ workspaceId }); | ||
|
||
const key = decryptAssymmetric({ | ||
ciphertext: latestFileKey.latestKey.encryptedKey, | ||
nonce: latestFileKey.latestKey.nonce, | ||
publicKey: latestFileKey.latestKey.sender.publicKey, | ||
privateKey: localStorage.getItem("PRIVATE_KEY"), | ||
}); | ||
|
||
const randomBytes = crypto.randomBytes(16).toString('hex'); | ||
const { | ||
ciphertext, | ||
iv, | ||
tag, | ||
} = encryptSymmetric({ | ||
plaintext: key, | ||
key: randomBytes, | ||
}); | ||
|
||
let newServiceToken = await addServiceToken({ | ||
name: serviceTokenName, | ||
workspaceId, | ||
environment: envMapping[serviceTokenEnv], | ||
expiresIn: expiryMapping[serviceTokenExpiresIn], | ||
encryptedKey: ciphertext, | ||
iv, | ||
tag | ||
}); | ||
|
||
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData])); | ||
setServiceToken(newServiceToken.serviceToken + "." + randomBytes); | ||
}; | ||
|
||
function copyToClipboard() { | ||
// Get the text field | ||
var copyText = document.getElementById("serviceToken"); | ||
|
||
// Select the text field | ||
copyText.select(); | ||
copyText.setSelectionRange(0, 99999); // For mobile devices | ||
|
||
// Copy the text inside the text field | ||
navigator.clipboard.writeText(copyText.value); | ||
|
||
setServiceTokenCopied(true); | ||
setTimeout(() => setServiceTokenCopied(false), 2000); | ||
// Alert the copied text | ||
// alert("Copied the text: " + copyText.value); | ||
} | ||
|
||
const closeAddServiceTokenModal = () => { | ||
closeModal(); | ||
setServiceTokenName(""); | ||
setServiceToken(""); | ||
}; | ||
|
||
return ( | ||
<div className="z-50"> | ||
<Transition appear show={isOpen} as={Fragment}> | ||
<Dialog as="div" className="relative" onClose={closeModal}> | ||
<Transition.Child | ||
as={Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0" | ||
enterTo="opacity-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100" | ||
leaveTo="opacity-0" | ||
> | ||
<div className="fixed inset-0 bg-bunker-700 bg-opacity-80" /> | ||
</Transition.Child> | ||
|
||
<div className="fixed inset-0 overflow-y-auto"> | ||
<div className="flex min-h-full items-center justify-center p-4 text-center"> | ||
<Transition.Child | ||
as={Fragment} | ||
enter="ease-out duration-300" | ||
enterFrom="opacity-0 scale-95" | ||
enterTo="opacity-100 scale-100" | ||
leave="ease-in duration-200" | ||
leaveFrom="opacity-100 scale-100" | ||
leaveTo="opacity-0 scale-95" | ||
> | ||
{serviceToken == "" ? ( | ||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> | ||
<Dialog.Title | ||
as="h3" | ||
className="text-lg font-medium leading-6 text-gray-400 z-50" | ||
> | ||
{t("section-token:add-dialog.title", { | ||
target: workspaceName, | ||
})} | ||
</Dialog.Title> | ||
<div className="mt-2 mb-4"> | ||
<div className="flex flex-col"> | ||
<p className="text-sm text-gray-500"> | ||
{t("section-token:add-dialog.description")} | ||
</p> | ||
</div> | ||
</div> | ||
<div className="max-h-28 mb-2"> | ||
<InputField | ||
label={t("section-token:add-dialog.name")} | ||
onChangeHandler={setServiceTokenName} | ||
type="varName" | ||
value={serviceTokenName} | ||
placeholder="" | ||
isRequired | ||
/> | ||
</div> | ||
<div className="max-h-28 mb-2"> | ||
<ListBox | ||
selected={serviceTokenEnv} | ||
onChange={setServiceTokenEnv} | ||
data={[ | ||
"Development", | ||
"Staging", | ||
"Production", | ||
"Testing", | ||
]} | ||
isFull={true} | ||
text={`${t("common:environment")}: `} | ||
/> | ||
</div> | ||
<div className="max-h-28"> | ||
<ListBox | ||
selected={serviceTokenExpiresIn} | ||
onChange={setServiceTokenExpiresIn} | ||
data={[ | ||
"1 day", | ||
"7 days", | ||
"1 month", | ||
"6 months", | ||
"12 months", | ||
]} | ||
isFull={true} | ||
text={`${t("common:expired-in")}: `} | ||
/> | ||
</div> | ||
<div className="max-w-max"> | ||
<div className="mt-6 flex flex-col justify-start w-max"> | ||
<Button | ||
onButtonPressed={() => generateServiceToken()} | ||
color="mineshaft" | ||
text={t("section-token:add-dialog.add")} | ||
textDisabled={t("section-token:add-dialog.add")} | ||
size="md" | ||
active={serviceTokenName == "" ? false : true} | ||
/> | ||
</div> | ||
</div> | ||
</Dialog.Panel> | ||
) : ( | ||
<Dialog.Panel className="w-full max-w-md transform rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all"> | ||
<Dialog.Title | ||
as="h3" | ||
className="text-lg font-medium leading-6 text-gray-400 z-50" | ||
> | ||
{t("section-token:add-dialog.copy-service-token")} | ||
</Dialog.Title> | ||
<div className="mt-2 mb-4"> | ||
<div className="flex flex-col"> | ||
<p className="text-sm text-gray-500"> | ||
{t( | ||
"section-token:add-dialog.copy-service-token-description" | ||
)} | ||
</p> | ||
</div> | ||
</div> | ||
<div className="w-full"> | ||
<div className="flex justify-end items-center bg-white/[0.07] text-base mt-2 mr-2 rounded-md text-gray-400 w-full h-20"> | ||
<input | ||
type="text" | ||
value={serviceToken} | ||
id="serviceToken" | ||
className="invisible bg-white/0 text-gray-400 py-2 w-full px-2 min-w-full outline-none" | ||
></input> | ||
<div className="bg-white/0 max-w-md text-sm text-gray-400 py-2 w-full pl-14 pr-2 break-words outline-none"> | ||
{serviceToken} | ||
</div> | ||
<div className="group font-normal h-full relative inline-block text-gray-400 underline hover:text-primary duration-200"> | ||
<button | ||
onClick={copyToClipboard} | ||
className="h-full pl-3.5 pr-4 border-l border-white/20 py-2 hover:bg-white/[0.12] duration-200" | ||
> | ||
{serviceTokenCopied ? ( | ||
<FontAwesomeIcon | ||
icon={faCheck} | ||
className="pr-0.5" | ||
/> | ||
) : ( | ||
<FontAwesomeIcon icon={faCopy} /> | ||
)} | ||
</button> | ||
<span className="absolute hidden group-hover:flex group-hover:animate-popup duration-300 w-28 -left-8 -top-20 translate-y-full px-3 py-2 bg-chicago-900 rounded-md text-center text-gray-400 text-sm"> | ||
{t("common:click-to-copy")} | ||
</span> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="mt-6 flex flex-col justify-start w-max"> | ||
<Button | ||
onButtonPressed={() => closeAddServiceTokenModal()} | ||
color="mineshaft" | ||
text="Close" | ||
size="md" | ||
/> | ||
</div> | ||
</Dialog.Panel> | ||
)} | ||
</Transition.Child> | ||
</div> | ||
</div> | ||
</Dialog> | ||
</Transition> | ||
</div> | ||
); | ||
}; | ||
|
||
export default AddApiKeyDialog; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { faX } from '@fortawesome/free-solid-svg-icons'; | ||
|
||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; | ||
|
||
import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; | ||
import { reverseEnvMapping } from '../../../public/data/frequentConstants'; | ||
import guidGenerator from '../../utilities/randomId'; | ||
import Button from '../buttons/Button'; | ||
|
||
interface TokenProps { | ||
_id: string; | ||
name: string; | ||
environment: string; | ||
expiresAt: string; | ||
} | ||
|
||
interface ServiceTokensProps { | ||
data: TokenProps[]; | ||
setServiceTokens: (value: TokenProps[]) => void; | ||
} | ||
|
||
/** | ||
* This is the component that we utilize for the api key table | ||
* @param {object} obj | ||
* @param {any[]} obj.data - current state of the api key table | ||
* @param {function} obj.setServiceTokens - updating the state of the api key table | ||
* @returns | ||
*/ | ||
const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => { | ||
const { createNotification } = useNotificationContext(); | ||
|
||
return ( | ||
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> | ||
<div className="absolute rounded-t-md w-full h-12 bg-white/5"></div> | ||
<table className="w-full my-1"> | ||
<thead className="text-bunker-300 text-sm font-light"> | ||
<tr> | ||
<th className="text-left pl-6 pt-2.5 pb-2">API KEY NAME</th> | ||
<th className="text-left pl-6 pt-2.5 pb-2">ENVIRONMENT</th> | ||
<th className="text-left pl-6 pt-2.5 pb-2">VAILD UNTIL</th> | ||
<th></th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{data?.length > 0 ? ( | ||
data?.map((row) => { | ||
return ( | ||
<tr | ||
key={guidGenerator()} | ||
className="bg-bunker-800 hover:bg-bunker-800/5 duration-100" | ||
> | ||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> | ||
{row.name} | ||
</td> | ||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> | ||
{reverseEnvMapping[row.environment]} | ||
</td> | ||
<td className="pl-6 py-2 border-mineshaft-700 border-t text-gray-300"> | ||
{new Date(row.expiresAt).toUTCString()} | ||
</td> | ||
<td className="py-2 border-mineshaft-700 border-t"> | ||
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> | ||
<Button | ||
onButtonPressed={() => { | ||
deleteServiceToken({ serviceTokenId: row._id} ); | ||
setServiceTokens(data.filter(token => token._id != row._id)); | ||
createNotification({ | ||
text: `'${row.name}' token has been revoked.`, | ||
type: 'error' | ||
}); | ||
}} | ||
color="red" | ||
size="icon-sm" | ||
icon={faX} | ||
/> | ||
</div> | ||
</td> | ||
</tr> | ||
); | ||
}) | ||
) : ( | ||
<tr> | ||
<td colSpan={4} className="text-center pt-7 pb-5 text-bunker-300 text-sm"> | ||
No API keys yet | ||
</td> | ||
</tr> | ||
)} | ||
</tbody> | ||
</table> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ApiKeyTable; |
Oops, something went wrong.