Merge pull request #223 from Infisical/api-keys

API Keys V1
This commit is contained in:
BlackMagiq
2023-01-15 14:49:21 +07:00
committed by GitHub
17 changed files with 574 additions and 147 deletions

View File

@@ -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))

View File

@@ -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]

View File

@@ -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]

View File

@@ -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
});
}
/**

View File

@@ -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;
}

View File

@@ -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]

View File

@@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2';
router.get(
'/me',
requireAuth({
acceptedAuthModes: ['jwt']
acceptedAuthModes: ['jwt', 'apiKey']
}),
usersController.getMe
);

View File

@@ -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],

View 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;

View 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;

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;

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;

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;

View File

@@ -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

View File

@@ -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"
]);

View 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"
}
}

View File

@@ -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"
}
}