mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
@@ -111,7 +111,7 @@ app.use('/api/v2/workspace', v2WorkspaceRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/secret', v2SecretRouter); // stop supporting, TODO: revise
|
||||
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||
app.use('/api/v2/api-key-data', v2APIKeyDataRouter);
|
||||
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||
|
||||
// api docs
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ADMIN, MEMBER } from '../../../variables';
|
||||
router.get(
|
||||
'/:secretId/secret-versions',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -27,7 +27,7 @@ router.get(
|
||||
router.post(
|
||||
'/:secretId/secret-versions/rollback',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -12,7 +12,7 @@ import { workspaceController } from '../../controllers/v1';
|
||||
router.get(
|
||||
'/:workspaceId/secret-snapshots',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -40,7 +40,7 @@ router.get(
|
||||
router.post(
|
||||
'/:workspaceId/secret-snapshots/rollback',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -54,7 +54,7 @@ router.post(
|
||||
router.get(
|
||||
'/:workspaceId/logs',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -16,49 +16,66 @@ import {
|
||||
AccountNotFoundError,
|
||||
ServiceTokenDataNotFoundError,
|
||||
APIKeyDataNotFoundError,
|
||||
UnauthorizedRequestError
|
||||
UnauthorizedRequestError,
|
||||
BadRequestError
|
||||
} from '../utils/errors';
|
||||
|
||||
// TODO 1: check if API key works
|
||||
// TODO 2: optimize middleware
|
||||
|
||||
/**
|
||||
* Validate that auth token value [authTokenValue] falls under one of
|
||||
* accepted auth modes [acceptedAuthModes].
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.authTokenValue - auth token value (e.g. JWT or service token value)
|
||||
* @param {String[]} obj.acceptedAuthModes - accepted auth modes (e.g. jwt, serviceToken)
|
||||
* @returns {String} authMode - auth mode
|
||||
* @param {Object} obj.headers - HTTP request headers object
|
||||
*/
|
||||
const validateAuthMode = ({
|
||||
authTokenValue,
|
||||
headers,
|
||||
acceptedAuthModes
|
||||
}: {
|
||||
authTokenValue: string;
|
||||
acceptedAuthModes: string[];
|
||||
headers: { [key: string]: string | string[] | undefined },
|
||||
acceptedAuthModes: string[]
|
||||
}) => {
|
||||
let authMode;
|
||||
try {
|
||||
switch (authTokenValue.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
authMode = 'serviceToken';
|
||||
break;
|
||||
case 'ak':
|
||||
authMode = 'apiKey';
|
||||
break;
|
||||
default:
|
||||
authMode = 'jwt';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!acceptedAuthModes.includes(authMode))
|
||||
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
|
||||
// TODO: refactor middleware
|
||||
const apiKey = headers['x-api-key'];
|
||||
const authHeader = headers['authorization'];
|
||||
|
||||
} catch (err) {
|
||||
throw UnauthorizedRequestError({ message: 'Failed to authenticated auth mode' });
|
||||
let authTokenType, authTokenValue;
|
||||
if (apiKey === undefined && authHeader === undefined) {
|
||||
// case: no auth or X-API-KEY header present
|
||||
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
||||
}
|
||||
|
||||
return authMode;
|
||||
if (typeof apiKey === 'string') {
|
||||
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
||||
authTokenType = 'apiKey';
|
||||
authTokenValue = apiKey;
|
||||
}
|
||||
|
||||
if (typeof authHeader === 'string') {
|
||||
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
|
||||
const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null]
|
||||
if (tokenType === null)
|
||||
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
|
||||
if (tokenType.toLowerCase() !== 'bearer')
|
||||
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
|
||||
if (tokenValue === null)
|
||||
throw BadRequestError({ message: 'Missing Authorization Body in the request header.' });
|
||||
|
||||
switch (tokenValue.split('.', 1)[0]) {
|
||||
case 'st':
|
||||
authTokenType = 'serviceToken';
|
||||
break;
|
||||
default:
|
||||
authTokenType = 'jwt';
|
||||
}
|
||||
authTokenValue = tokenValue;
|
||||
}
|
||||
|
||||
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
|
||||
|
||||
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
|
||||
|
||||
return ({
|
||||
authTokenType,
|
||||
authTokenValue
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getAuthSTDPayload,
|
||||
getAuthAPIKeyPayload
|
||||
} from '../helpers/auth';
|
||||
import { BadRequestError } from '../utils/errors';
|
||||
|
||||
declare module 'jsonwebtoken' {
|
||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||
@@ -31,37 +30,28 @@ const requireAuth = ({
|
||||
acceptedAuthModes: string[];
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||
if (AUTH_TOKEN_TYPE === null)
|
||||
return next(BadRequestError({ message: `Missing Authorization Header in the request header.` }))
|
||||
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer')
|
||||
return next(BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }))
|
||||
if (AUTH_TOKEN_VALUE === null)
|
||||
return next(BadRequestError({ message: 'Missing Authorization Body in the request header' }))
|
||||
|
||||
// validate auth token against
|
||||
const authMode = validateAuthMode({
|
||||
authTokenValue: AUTH_TOKEN_VALUE,
|
||||
// validate auth token against accepted auth modes [acceptedAuthModes]
|
||||
// and return token type [authTokenType] and value [authTokenValue]
|
||||
const { authTokenType, authTokenValue } = validateAuthMode({
|
||||
headers: req.headers,
|
||||
acceptedAuthModes
|
||||
});
|
||||
|
||||
if (!acceptedAuthModes.includes(authMode)) throw new Error('Failed to validate auth mode');
|
||||
|
||||
// attach auth payloads
|
||||
switch (authMode) {
|
||||
switch (authTokenType) {
|
||||
case 'serviceToken':
|
||||
req.serviceTokenData = await getAuthSTDPayload({
|
||||
authTokenValue: AUTH_TOKEN_VALUE
|
||||
authTokenValue
|
||||
});
|
||||
break;
|
||||
case 'apiKey':
|
||||
req.user = await getAuthAPIKeyPayload({
|
||||
authTokenValue: AUTH_TOKEN_VALUE
|
||||
authTokenValue
|
||||
});
|
||||
break;
|
||||
default:
|
||||
req.user = await getAuthUserPayload({
|
||||
authTokenValue: AUTH_TOKEN_VALUE
|
||||
authTokenValue
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ router.post(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -76,7 +76,7 @@ router.get(
|
||||
query('environment').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt', 'serviceToken']
|
||||
acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -115,7 +115,7 @@ router.patch(
|
||||
}),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -143,7 +143,7 @@ router.delete(
|
||||
.isEmpty(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireSecretsAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
|
||||
@@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2';
|
||||
router.get(
|
||||
'/me',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
usersController.getMe
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ router.get(
|
||||
router.get(
|
||||
'/:workspaceId/encrypted-key',
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER]
|
||||
@@ -75,7 +75,7 @@ router.get( // new - TODO: rewire dashboard to this route
|
||||
param('workspaceId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
@@ -89,7 +89,7 @@ router.delete( // TODO - rewire dashboard to this route
|
||||
param('membershipId').exists().trim(),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
@@ -107,7 +107,7 @@ router.patch( // TODO - rewire dashboard to this route
|
||||
body('role').exists().isString().trim().isIn([ADMIN, MEMBER]),
|
||||
validateRequest,
|
||||
requireAuth({
|
||||
acceptedAuthModes: ['jwt']
|
||||
acceptedAuthModes: ['jwt', 'apiKey']
|
||||
}),
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN],
|
||||
|
||||
217
frontend/components/basic/dialog/AddApiKeyDialog.js
Normal file
217
frontend/components/basic/dialog/AddApiKeyDialog.js
Normal file
@@ -0,0 +1,217 @@
|
||||
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 addAPIKey from "~/pages/api/apiKey/addAPIKey";
|
||||
|
||||
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');
|
||||
|
||||
// TODO: convert to TS
|
||||
const AddApiKeyDialog = ({
|
||||
isOpen,
|
||||
closeModal,
|
||||
apiKeys,
|
||||
setApiKeys
|
||||
}) => {
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [apiKeyName, setApiKeyName] = useState("");
|
||||
const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day");
|
||||
const [apiKeyCopied, setApiKeyCopied] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const generateAPIKey = async () => {
|
||||
const newApiKey = await addAPIKey({
|
||||
name: apiKeyName,
|
||||
expiresIn: expiryMapping[apiKeyExpiresIn]
|
||||
});
|
||||
|
||||
setApiKeys([...apiKeys, newApiKey.apiKeyData])
|
||||
setApiKey(newApiKey.apiKey);
|
||||
};
|
||||
|
||||
function copyToClipboard() {
|
||||
// Get the text field
|
||||
var copyText = document.getElementById("apiKey");
|
||||
|
||||
// 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);
|
||||
|
||||
setApiKeyCopied(true);
|
||||
setTimeout(() => setApiKeyCopied(false), 2000);
|
||||
// Alert the copied text
|
||||
// alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
const closeAddApiKeyModal = () => {
|
||||
closeModal();
|
||||
setApiKeyName("");
|
||||
setApiKey("");
|
||||
};
|
||||
|
||||
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"
|
||||
>
|
||||
{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-api-key:add-dialog.title")}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2 mb-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("section-api-key:add-dialog.description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-28 mb-2">
|
||||
<InputField
|
||||
label={t("section-api-key:add-dialog.name")}
|
||||
onChangeHandler={setApiKeyName}
|
||||
type="varName"
|
||||
value={apiKeyName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-28">
|
||||
<ListBox
|
||||
selected={apiKeyExpiresIn}
|
||||
onChange={setApiKeyExpiresIn}
|
||||
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={() => generateAPIKey()}
|
||||
color="mineshaft"
|
||||
text={t("section-api-key:add-dialog.add")}
|
||||
textDisabled={t("section-api-key:add-dialog.add")}
|
||||
size="md"
|
||||
active={apiKeyName == "" ? 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-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-api-key: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={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">
|
||||
{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"
|
||||
>
|
||||
{apiKeyCopied ? (
|
||||
<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={() => closeAddApiKeyModal()}
|
||||
color="mineshaft"
|
||||
text="Close"
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
)}
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddApiKeyDialog;
|
||||
87
frontend/components/basic/table/ApiKeyTable.tsx
Normal file
87
frontend/components/basic/table/ApiKeyTable.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { faX } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider';
|
||||
|
||||
import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey";
|
||||
import guidGenerator from '../../utilities/randomId';
|
||||
import Button from '../buttons/Button';
|
||||
|
||||
interface TokenProps {
|
||||
_id: string;
|
||||
name: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ServiceTokensProps {
|
||||
data: TokenProps[];
|
||||
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.setApiKeys - updating the state of the api key table
|
||||
* @returns
|
||||
*/
|
||||
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>
|
||||
<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">VALID 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">
|
||||
{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={() => {
|
||||
deleteAPIKey({ apiKeyId: row._id} );
|
||||
setApiKeys(data.filter(token => token._id != row._id));
|
||||
createNotification({
|
||||
text: `'${row.name}' API key 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;
|
||||
37
frontend/pages/api/apiKey/addAPIKey.ts
Normal file
37
frontend/pages/api/apiKey/addAPIKey.ts
Normal 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
frontend/pages/api/apiKey/deleteAPIKey.ts
Normal file
30
frontend/pages/api/apiKey/deleteAPIKey.ts
Normal 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
frontend/pages/api/apiKey/getAPIKeys.ts
Normal file
26
frontend/pages/api/apiKey/getAPIKeys.ts
Normal 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
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import { faCheck, faPlus, faX } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import Button from '~/components/basic/buttons/Button';
|
||||
import InputField from '~/components/basic/InputField';
|
||||
import ListBox from '~/components/basic/Listbox';
|
||||
import NavHeader from '~/components/navigation/NavHeader';
|
||||
import changePassword from '~/components/utilities/cryptography/changePassword';
|
||||
import issueBackupKey from '~/components/utilities/cryptography/issueBackupKey';
|
||||
import passwordCheck from '~/utilities/checks/PasswordCheck';
|
||||
import { getTranslatedServerSideProps } from '~/utilities/withTranslateProps';
|
||||
import Button from "~/components/basic/buttons/Button";
|
||||
import InputField from "~/components/basic/InputField";
|
||||
import ListBox from "~/components/basic/Listbox";
|
||||
import ApiKeyTable from "~/components/basic/table/ApiKeyTable";
|
||||
import NavHeader from "~/components/navigation/NavHeader";
|
||||
import changePassword from "~/components/utilities/cryptography/changePassword";
|
||||
import issueBackupKey from "~/components/utilities/cryptography/issueBackupKey";
|
||||
import passwordCheck from "~/utilities/checks/PasswordCheck";
|
||||
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
|
||||
|
||||
import getUser from '../../api/user/getUser';
|
||||
import AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog";
|
||||
import getAPIKeys from "../../api/apiKey/getAPIKeys";
|
||||
import getUser from "../../api/user/getUser";
|
||||
|
||||
export default function PersonalSettings() {
|
||||
const [personalEmail, setPersonalEmail] = useState('');
|
||||
@@ -29,6 +32,8 @@ export default function PersonalSettings() {
|
||||
const [passwordChanged, setPasswordChanged] = useState(false);
|
||||
const [backupKeyIssued, setBackupKeyIssued] = useState(false);
|
||||
const [backupKeyError, setBackupKeyError] = useState(false);
|
||||
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false)
|
||||
const [apiKeys, setApiKeys] = useState([]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
@@ -40,12 +45,26 @@ export default function PersonalSettings() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then((user) => {
|
||||
setPersonalEmail(user.email);
|
||||
setPersonalName(user.firstName + ' ' + user.lastName);
|
||||
});
|
||||
const load = async () => {
|
||||
try {
|
||||
const user = await getUser();
|
||||
setApiKeys(
|
||||
await getAPIKeys()
|
||||
);
|
||||
setPersonalEmail(user.email);
|
||||
setPersonalName(user.firstName + " " + user.lastName);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
}, []);
|
||||
|
||||
const closeAddApiKeyModal = () => {
|
||||
setIsAddApiKeyDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='bg-bunker-800 max-h-screen flex flex-col justify-between text-white'>
|
||||
<Head>
|
||||
@@ -54,8 +73,14 @@ export default function PersonalSettings() {
|
||||
</title>
|
||||
<link rel='icon' href='/infisical.ico' />
|
||||
</Head>
|
||||
<div className='flex flex-row'>
|
||||
<div className='w-full max-h-screen pb-2 overflow-y-auto'>
|
||||
<AddApiKeyDialog
|
||||
isOpen={isAddApiKeyDialogOpen}
|
||||
closeModal={closeAddApiKeyModal}
|
||||
apiKeys={apiKeys}
|
||||
setApiKeys={setApiKeys}
|
||||
/>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2 overflow-y-auto">
|
||||
<NavHeader
|
||||
pageName={t('settings-personal:title')}
|
||||
isProjectRelated={false}
|
||||
@@ -70,58 +95,10 @@ export default function PersonalSettings() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='min-w-md flex flex-col items-end pb-4'>
|
||||
{/* <div className="bg-white/5 rounded-md px-6 py-4 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="max-h-28 w-full max-w-md mr-auto">
|
||||
<p className="font-semibold mr-4 text-gray-200 text-xl mb-2">
|
||||
Display Name
|
||||
</p>
|
||||
<InputField
|
||||
onChangeHandler={modifyOrgName}
|
||||
type="varName"
|
||||
value={orgName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-start w-full">
|
||||
<div
|
||||
className={`flex justify-start max-w-sm mt-4 mb-2 rounded-md bg-gray-800 text-sm ${
|
||||
buttonReady &&
|
||||
"hover:bg-primary hover:text-black hover:text-semibold duration-200 cursor-pointer"
|
||||
} text-gray-400 px-4 py-2.5`}
|
||||
>
|
||||
{buttonReady ? (
|
||||
<button
|
||||
type="button"
|
||||
className="flex flex-start justify-center font-medium px-2"
|
||||
onClick={() =>
|
||||
submitChanges(orgName)
|
||||
}
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex flex-row items-center jutify-center px-4">
|
||||
<FontAwesomeIcon
|
||||
className="text-lg mr-3 text-gray-400"
|
||||
icon={faCheck}
|
||||
/>
|
||||
<p className="font-base">
|
||||
Saved
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start w-full mb-6 mt-4'>
|
||||
<p className='text-xl font-semibold self-start'>
|
||||
{t('settings-personal:change-language')}
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-5xl">
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
|
||||
<p className="text-xl font-semibold self-start">
|
||||
{t("settings-personal:change-language")}
|
||||
</p>
|
||||
<div className='max-h-28 w-ful mt-4'>
|
||||
<ListBox
|
||||
@@ -132,6 +109,33 @@ export default function PersonalSettings() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-2 mb-8 pt-2">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<div className="flex flex-col w-full">
|
||||
<p className="text-xl font-semibold mb-3">
|
||||
{t("settings-personal:api-keys.title")}
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("settings-personal:api-keys.description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-48 mt-2">
|
||||
<Button
|
||||
text={String(t("settings-personal:api-keys.add-new"))}
|
||||
onButtonPressed={() => {
|
||||
setIsAddApiKeyDialogOpen(true);
|
||||
}}
|
||||
color="mineshaft"
|
||||
icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ApiKeyTable
|
||||
data={apiKeys}
|
||||
setApiKeys={setApiKeys as any}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='bg-white/5 rounded-md px-6 pt-5 pb-6 flex flex-col items-start w-full mb-6'>
|
||||
<div className='flex flex-row max-w-5xl justify-between items-center w-full'>
|
||||
@@ -295,11 +299,11 @@ export default function PersonalSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='bg-white/5 rounded-md px-6 pt-5 pb-6 mt-4 flex flex-col items-start w-full mb-6'>
|
||||
<div className='flex flex-row max-w-5xl justify-between items-center w-full'>
|
||||
<div className='flex flex-col justify-between w-full max-w-3xl'>
|
||||
<p className='text-xl font-semibold mb-3 min-w-max'>
|
||||
{t('settings-personal:emergency.name')}
|
||||
<div className="bg-white/5 rounded-md px-6 pt-5 pb-6 mt-2 flex flex-col items-start flex flex-col items-start w-full mb-6">
|
||||
<div className="flex flex-row max-w-5xl justify-between items-center w-full">
|
||||
<div className="flex flex-col justify-between w-full max-w-3xl">
|
||||
<p className="text-xl font-semibold mb-3 min-w-max">
|
||||
{t("settings-personal:emergency.name")}
|
||||
</p>
|
||||
<p className='text-sm text-mineshaft-300 min-w-max'>
|
||||
{t('settings-personal:emergency.text1')}
|
||||
@@ -359,7 +363,8 @@ export default function PersonalSettings() {
|
||||
PersonalSettings.requireAuth = true;
|
||||
|
||||
export const getServerSideProps = getTranslatedServerSideProps([
|
||||
'settings',
|
||||
'settings-personal',
|
||||
'section-password',
|
||||
"settings",
|
||||
"settings-personal",
|
||||
"section-password",
|
||||
"section-api-key"
|
||||
]);
|
||||
|
||||
13
frontend/public/locales/en/section-api-key.json
Normal file
13
frontend/public/locales/en/section-api-key.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,10 @@
|
||||
"text2": "Only the latest issued Emergency Kit remains valid. To get a new Emergency Kit, verify your password.",
|
||||
"download": "Download Emergency Kit"
|
||||
},
|
||||
"change-language": "Change Language"
|
||||
"change-language": "Change Language",
|
||||
"api-keys": {
|
||||
"title": "API Keys",
|
||||
"description": "Manage your personal API Keys to access the Infisical API.",
|
||||
"add-new": "Add new"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user