diff --git a/backend/src/app.ts b/backend/src/app.ts index d32e83be7..e1ea949b9 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -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)) diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index ac3089e33..8e93919a0 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -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] diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index c9da58261..a799d073b 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -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] diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 2b972c09d..fa1e50aa6 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -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 + }); } /** diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 3be8418ee..24ef92f06 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -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; } diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index ccdce5321..45a273d35 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -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] diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index dba107b15..95b064a79 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -8,7 +8,7 @@ import { usersController } from '../../controllers/v2'; router.get( '/me', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ['jwt', 'apiKey'] }), usersController.getMe ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index ca920e15a..42c0f16a4 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -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], diff --git a/frontend/components/basic/dialog/AddApiKeyDialog.js b/frontend/components/basic/dialog/AddApiKeyDialog.js new file mode 100644 index 000000000..99bbbd9dc --- /dev/null +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -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 ( +
+ + + +
+ + +
+
+ + {apiKey == "" ? ( + + + {t("section-api-key:add-dialog.title")} + +
+
+

+ {t("section-api-key:add-dialog.description")} +

+
+
+
+ +
+
+ +
+
+
+
+
+
+ ) : ( + + + {t("section-api-key:add-dialog.copy-service-token")} + +
+
+

+ {t( + "section-api-key:add-dialog.copy-service-token-description" + )} +

+
+
+
+
+ +
+ {apiKey} +
+
+ + + {t("common:click-to-copy")} + +
+
+
+
+
+
+ )} +
+
+
+
+
+
+ ); +}; + +export default AddApiKeyDialog; diff --git a/frontend/components/basic/table/ApiKeyTable.tsx b/frontend/components/basic/table/ApiKeyTable.tsx new file mode 100644 index 000000000..dd71b1b3f --- /dev/null +++ b/frontend/components/basic/table/ApiKeyTable.tsx @@ -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 ( +
+
+ + + + + + + + + + {data?.length > 0 ? ( + data?.map((row) => { + return ( + + + + + + ); + }) + ) : ( + + + + )} + +
API KEY NAMEVALID UNTIL
+ {row.name} + + {new Date(row.expiresAt).toUTCString()} + +
+
+
+ No API keys yet +
+
+ ); +}; + +export default ApiKeyTable; diff --git a/frontend/pages/api/apiKey/addAPIKey.ts b/frontend/pages/api/apiKey/addAPIKey.ts new file mode 100644 index 000000000..4707af9be --- /dev/null +++ b/frontend/pages/api/apiKey/addAPIKey.ts @@ -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; diff --git a/frontend/pages/api/apiKey/deleteAPIKey.ts b/frontend/pages/api/apiKey/deleteAPIKey.ts new file mode 100644 index 000000000..cea53444d --- /dev/null +++ b/frontend/pages/api/apiKey/deleteAPIKey.ts @@ -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; diff --git a/frontend/pages/api/apiKey/getAPIKeys.ts b/frontend/pages/api/apiKey/getAPIKeys.ts new file mode 100644 index 000000000..0168a87c6 --- /dev/null +++ b/frontend/pages/api/apiKey/getAPIKeys.ts @@ -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; diff --git a/frontend/pages/api/serviceToken/addServiceToken.ts b/frontend/pages/api/serviceToken/addServiceToken.ts index 91019f413..c9cc7a239 100644 --- a/frontend/pages/api/serviceToken/addServiceToken.ts +++ b/frontend/pages/api/serviceToken/addServiceToken.ts @@ -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 diff --git a/frontend/pages/settings/personal/[id].tsx b/frontend/pages/settings/personal/[id].tsx index bae7a54a2..cd6428284 100644 --- a/frontend/pages/settings/personal/[id].tsx +++ b/frontend/pages/settings/personal/[id].tsx @@ -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 (
@@ -54,8 +73,14 @@ export default function PersonalSettings() { -
-
+ +
+
-
-
-
- {/*
-
-

- Display Name -

- -
-
-
- {buttonReady ? ( - - ) : ( -
- -

- Saved -

-
- )} -
-
-
*/} -
-
-
-

- {t('settings-personal:change-language')} +

+
+

+ {t("settings-personal:change-language")}

+
+
+
+

+ {t("settings-personal:api-keys.title")} +

+

+ {t("settings-personal:api-keys.description")} +

+
+
+
+
+ +
@@ -295,11 +299,11 @@ export default function PersonalSettings() {
-
-
-
-

- {t('settings-personal:emergency.name')} +

+
+
+

+ {t("settings-personal:emergency.name")}

{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" ]); diff --git a/frontend/public/locales/en/section-api-key.json b/frontend/public/locales/en/section-api-key.json new file mode 100644 index 000000000..3419744fe --- /dev/null +++ b/frontend/public/locales/en/section-api-key.json @@ -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" + } +} diff --git a/frontend/public/locales/en/settings-personal.json b/frontend/public/locales/en/settings-personal.json index 8759c20f0..66a90b6c3 100644 --- a/frontend/public/locales/en/settings-personal.json +++ b/frontend/public/locales/en/settings-personal.json @@ -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" + } }