Complete v1 API Key

This commit is contained in:
Tuan Dang
2023-01-15 14:25:23 +07:00
parent 32928bf45c
commit 74b76eda7e
17 changed files with 250 additions and 183 deletions
@@ -4,9 +4,10 @@ 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 addAPIKey from "~/pages/api/apiKey/addAPIKey";
// import addServiceToken from "~/pages/api/serviceToken/addServiceToken";
// import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey";
import { envMapping } from "../../../public/data/frequentConstants";
import {
decryptAssymmetric,
@@ -26,58 +27,33 @@ const expiryMapping = {
const crypto = require('crypto');
// TODO: convert to TS
const AddApiKeyDialog = ({
isOpen,
closeModal,
workspaceId,
workspaceName,
serviceTokens,
setServiceTokens
apiKeys,
setApiKeys
}) => {
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 [apiKey, setApiKey] = useState("");
const [apiKeyName, setApiKeyName] = useState("");
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
const [apiKeyCopied, setApiKeyCopied] = 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
const generateAPIKey = async () => {
const newApiKey = await addAPIKey({
name: apiKeyName,
expiresIn: expiryMapping[apiKeyExpiresIn]
});
setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData]));
setServiceToken(newServiceToken.serviceToken + "." + randomBytes);
setApiKeys([...apiKeys, newApiKey.apiKeyData])
setApiKey(newApiKey.apiKey);
};
function copyToClipboard() {
// Get the text field
var copyText = document.getElementById("serviceToken");
var copyText = document.getElementById("apiKey");
// Select the text field
copyText.select();
@@ -86,16 +62,16 @@ const AddApiKeyDialog = ({
// Copy the text inside the text field
navigator.clipboard.writeText(copyText.value);
setServiceTokenCopied(true);
setTimeout(() => setServiceTokenCopied(false), 2000);
setApiKeyCopied(true);
setTimeout(() => setApiKeyCopied(false), 2000);
// Alert the copied text
// alert("Copied the text: " + copyText.value);
}
const closeAddServiceTokenModal = () => {
const closeAddApiKeyModal = () => {
closeModal();
setServiceTokenName("");
setServiceToken("");
setApiKeyName("");
setApiKey("");
};
return (
@@ -125,51 +101,37 @@ const AddApiKeyDialog = ({
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
{serviceToken == "" ? (
{apiKey == "" ? (
<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", {
{t("section-api-key: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")}
{t("section-api-key:add-dialog.description")}
</p>
</div>
</div>
<div className="max-h-28 mb-2">
<InputField
label={t("section-token:add-dialog.name")}
onChangeHandler={setServiceTokenName}
label={t("section-api-key:add-dialog.name")}
onChangeHandler={setApiKeyName}
type="varName"
value={serviceTokenName}
value={apiKeyName}
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}
selected={apiKeyExpiresIn}
onChange={setApiKeyExpiresIn}
data={[
"1 day",
"7 days",
@@ -184,12 +146,12 @@ const AddApiKeyDialog = ({
<div className="max-w-max">
<div className="mt-6 flex flex-col justify-start w-max">
<Button
onButtonPressed={() => generateServiceToken()}
onButtonPressed={() => generateAPIKey()}
color="mineshaft"
text={t("section-token:add-dialog.add")}
textDisabled={t("section-token:add-dialog.add")}
text={t("section-api-key:add-dialog.add")}
textDisabled={t("section-api-key:add-dialog.add")}
size="md"
active={serviceTokenName == "" ? false : true}
active={apiKeyName == "" ? false : true}
/>
</div>
</div>
@@ -200,13 +162,13 @@ const AddApiKeyDialog = ({
as="h3"
className="text-lg font-medium leading-6 text-gray-400 z-50"
>
{t("section-token:add-dialog.copy-service-token")}
{t("section-api-key: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"
"section-api-key:add-dialog.copy-service-token-description"
)}
</p>
</div>
@@ -215,19 +177,20 @@ const AddApiKeyDialog = ({
<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"
value={apiKey}
disabled={true}
id="apiKey"
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}
{apiKey}
</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 ? (
{apiKeyCopied ? (
<FontAwesomeIcon
icon={faCheck}
className="pr-0.5"
@@ -244,7 +207,7 @@ const AddApiKeyDialog = ({
</div>
<div className="mt-6 flex flex-col justify-start w-max">
<Button
onButtonPressed={() => closeAddServiceTokenModal()}
onButtonPressed={() => closeAddApiKeyModal()}
color="mineshaft"
text="Close"
size="md"
@@ -2,8 +2,7 @@ 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 deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button';
@@ -16,19 +15,18 @@ interface TokenProps {
interface ServiceTokensProps {
data: TokenProps[];
setServiceTokens: (value: TokenProps[]) => void;
setApiKeys: (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
* @param {function} obj.setApiKeys - updating the state of the api key table
* @returns
*/
const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
const ApiKeyTable = ({ data, setApiKeys }: 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>
@@ -36,8 +34,7 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<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 className="text-left pl-6 pt-2.5 pb-2">VALID UNTIL</th>
<th></th>
</tr>
</thead>
@@ -52,9 +49,6 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<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>
@@ -62,10 +56,10 @@ const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => {
<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));
deleteAPIKey({ apiKeyId: row._id} );
setApiKeys(data.filter(token => token._id != row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
text: `'${row.name}' API key has been revoked.`,
type: 'error'
});
}}
+37
View File
@@ -0,0 +1,37 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Props {
name: string;
expiresIn: number;
}
/**
* This route adds an API key for the user
* @param {object} obj
* @param {string} obj.name - name of the API key
* @param {string} obj.expiresIn - how soon the API key expires in ms
* @returns
*/
const addAPIKey = ({
name,
expiresIn,
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
expiresIn
})
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to add API key');
}
});
};
export default addAPIKey;
+30
View File
@@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Props {
apiKeyId: string;
}
/**
* This route revokes the API key with id [apiKeyId]
* @param {object} obj
* @param {string} obj.apiKeyId - id of the API key to delete
* @returns
*/
const deleteAPIKey = ({
apiKeyId
}: Props) => {
return SecurityClient.fetchCall('/api/v2/api-key/' + apiKeyId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
}).then(async (res) => {
if (res && res.status == 200) {
return (await res.json());
} else {
console.log('Failed to delete API key');
}
});
};
export default deleteAPIKey;
+26
View File
@@ -0,0 +1,26 @@
import SecurityClient from '~/utilities/SecurityClient';
/**
* This route gets API keys for the user
* @param {*} param0
* @returns
*/
const getAPIKeys = () => {
return SecurityClient.fetchCall(
'/api/v2/api-key',
{
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}
).then(async (res) => {
if (res && res.status == 200) {
return (await res.json()).apiKeyData;
} else {
console.log('Failed to get API keys');
}
});
};
export default getAPIKeys;
@@ -11,7 +11,7 @@ interface Props {
}
/**
* This route gets service tokens for a specific user in a project
* This route adds a service token for a specific user in a project
* @param {object} obj
* @param {string} obj.name - name of the service token
* @param {string} obj.workspaceId - workspace for which we are issuing the token
+21 -19
View File
@@ -16,6 +16,8 @@ import passwordCheck from "~/utilities/checks/PasswordCheck";
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
import deleteAPIKey from "../../api/apiKey/deleteAPIKey";
import getAPIKeys from "../../api/apiKey/getAPIKeys";
import getUser from "../../api/user/getUser";
export default function PersonalSettings() {
@@ -43,10 +45,21 @@ export default function PersonalSettings() {
localStorage.setItem("lang", to);
};
useEffect(async () => {
let user = await getUser();
setPersonalEmail(user.email);
setPersonalName(user.firstName + " " + user.lastName);
useEffect(() => {
const load = async () => {
try {
let user = await getUser();
setApiKeys(
await getAPIKeys()
);
setPersonalEmail(user.email);
setPersonalName(user.firstName + " " + user.lastName);
} catch (err) {
console.error(err);
}
}
load();
}, []);
const closeAddApiKeyModal = () => {
@@ -63,10 +76,9 @@ export default function PersonalSettings() {
</Head>
<AddApiKeyDialog
isOpen={isAddApiKeyDialogOpen}
workspaceId={router.query.id}
closeModal={closeAddApiKeyModal}
serviceTokens={apiKeys}
setServiceTokens={setApiKeys}
apiKeys={apiKeys}
setApiKeys={setApiKeys}
/>
<div className="flex flex-row">
<div className="w-full max-h-screen pb-2 overflow-y-auto">
@@ -108,17 +120,6 @@ export default function PersonalSettings() {
<p className="text-sm text-gray-400">
{t("settings-personal:api-keys.description")}
</p>
<p className="text-sm text-gray-400 mb-4">
Please, make sure you are on the
<a
className="text-primary underline underline-offset-2 ml-1"
href="https://infisical.com/docs/cli/overview"
target="_blank"
rel="noreferrer"
>
latest version of CLI
</a>.
</p>
</div>
<div className="w-48 mt-2">
<Button
@@ -134,7 +135,7 @@ export default function PersonalSettings() {
</div>
<ApiKeyTable
data={apiKeys}
setServiceTokens={setApiKeys}
setApiKeys={setApiKeys}
/>
</div>
@@ -365,4 +366,5 @@ export const getServerSideProps = getTranslatedServerSideProps([
"settings",
"settings-personal",
"section-password",
"section-api-key"
]);
@@ -0,0 +1,13 @@
{
"api-keys": "Service Tokens",
"api-keys-description": "Every service token is specific to you, a certain project and a certain environment within this project.",
"add-new": "Add New Token",
"add-dialog": {
"title": "Add an API Key",
"description": "Specify the name and expiry period. When an API key is generated, you will only be able to see it once before it disappears. Make sure to save it somewhere.",
"name": "API Key Name",
"add": "Add API Key",
"copy-service-token": "Copy your API key",
"copy-service-token-description": "Once you close this popup, you will never see your API key again"
}
}