From efd5016977f9a951bae9a061cd5108e78a300df7 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Thu, 12 Jan 2023 17:08:40 -0800 Subject: [PATCH 1/5] Added frontend for api-keys --- .../basic/dialog/AddApiKeyDialog.js | 264 ++++++++++++++++++ .../components/basic/table/ApiKeyTable.tsx | 94 +++++++ frontend/pages/settings/personal/[id].js | 105 +++---- .../public/locales/en/settings-personal.json | 7 +- 4 files changed, 419 insertions(+), 51 deletions(-) create mode 100644 frontend/components/basic/dialog/AddApiKeyDialog.js create mode 100644 frontend/components/basic/table/ApiKeyTable.tsx diff --git a/frontend/components/basic/dialog/AddApiKeyDialog.js b/frontend/components/basic/dialog/AddApiKeyDialog.js new file mode 100644 index 000000000..3c9c35938 --- /dev/null +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -0,0 +1,264 @@ +import { Fragment, useState } from "react"; +import { useTranslation } from "next-i18next"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dialog, Transition } from "@headlessui/react"; + +import addServiceToken from "~/pages/api/serviceToken/addServiceToken"; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; + +import { envMapping } from "../../../public/data/frequentConstants"; +import { + decryptAssymmetric, + encryptSymmetric, +} from "../../utilities/cryptography/crypto"; +import Button from "../buttons/Button"; +import InputField from "../InputField"; +import ListBox from "../Listbox"; + +const expiryMapping = { + "1 day": 86400, + "7 days": 604800, + "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000, +}; + +const crypto = require('crypto'); + +const AddApiKeyDialog = ({ + isOpen, + closeModal, + workspaceId, + workspaceName, + serviceTokens, + setServiceTokens +}) => { + const [serviceToken, setServiceToken] = useState(""); + const [serviceTokenName, setServiceTokenName] = useState(""); + const [serviceTokenEnv, setServiceTokenEnv] = useState("Development"); + const [serviceTokenExpiresIn, setServiceTokenExpiresIn] = useState("1 day"); + const [serviceTokenCopied, setServiceTokenCopied] = useState(false); + const { t } = useTranslation(); + + const generateServiceToken = async () => { + const latestFileKey = await getLatestFileKey({ workspaceId }); + + const key = decryptAssymmetric({ + ciphertext: latestFileKey.latestKey.encryptedKey, + nonce: latestFileKey.latestKey.nonce, + publicKey: latestFileKey.latestKey.sender.publicKey, + privateKey: localStorage.getItem("PRIVATE_KEY"), + }); + + const randomBytes = crypto.randomBytes(16).toString('hex'); + const { + ciphertext, + iv, + tag, + } = encryptSymmetric({ + plaintext: key, + key: randomBytes, + }); + + let newServiceToken = await addServiceToken({ + name: serviceTokenName, + workspaceId, + environment: envMapping[serviceTokenEnv], + expiresIn: expiryMapping[serviceTokenExpiresIn], + encryptedKey: ciphertext, + iv, + tag + }); + + setServiceTokens(serviceTokens.concat([newServiceToken.serviceTokenData])); + setServiceToken(newServiceToken.serviceToken + "." + randomBytes); + }; + + function copyToClipboard() { + // Get the text field + var copyText = document.getElementById("serviceToken"); + + // Select the text field + copyText.select(); + copyText.setSelectionRange(0, 99999); // For mobile devices + + // Copy the text inside the text field + navigator.clipboard.writeText(copyText.value); + + setServiceTokenCopied(true); + setTimeout(() => setServiceTokenCopied(false), 2000); + // Alert the copied text + // alert("Copied the text: " + copyText.value); + } + + const closeAddServiceTokenModal = () => { + closeModal(); + setServiceTokenName(""); + setServiceToken(""); + }; + + return ( +
+ + + +
+ + +
+
+ + {serviceToken == "" ? ( + + + {t("section-token:add-dialog.title", { + target: workspaceName, + })} + +
+
+

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

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

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

+
+
+
+
+ +
+ {serviceToken} +
+
+ + + {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..a7549e083 --- /dev/null +++ b/frontend/components/basic/table/ApiKeyTable.tsx @@ -0,0 +1,94 @@ +import { faX } from '@fortawesome/free-solid-svg-icons'; + +import { useNotificationContext } from '~/components/context/Notifications/NotificationProvider'; + +import deleteServiceToken from "../../../pages/api/serviceToken/deleteServiceToken"; +import { reverseEnvMapping } from '../../../public/data/frequentConstants'; +import guidGenerator from '../../utilities/randomId'; +import Button from '../buttons/Button'; + +interface TokenProps { + _id: string; + name: string; + environment: string; + expiresAt: string; +} + +interface ServiceTokensProps { + data: TokenProps[]; + setServiceTokens: (value: TokenProps[]) => void; +} + +/** + * This is the component that we utilize for the api key table + * @param {object} obj + * @param {any[]} obj.data - current state of the api key table + * @param {function} obj.setServiceTokens - updating the state of the api key table + * @returns + */ +const ApiKeyTable = ({ data, setServiceTokens }: ServiceTokensProps) => { + const { createNotification } = useNotificationContext(); + + return ( +
+
+ + + + + + + + + + + {data?.length > 0 ? ( + data?.map((row) => { + return ( + + + + + + + ); + }) + ) : ( + + + + )} + +
API KEY NAMEENVIRONMENTVAILD UNTIL
+ {row.name} + + {reverseEnvMapping[row.environment]} + + {new Date(row.expiresAt).toUTCString()} + +
+
+
+ No API keys yet +
+
+ ); +}; + +export default ApiKeyTable; diff --git a/frontend/pages/settings/personal/[id].js b/frontend/pages/settings/personal/[id].js index a592bf45b..d7441ec54 100644 --- a/frontend/pages/settings/personal/[id].js +++ b/frontend/pages/settings/personal/[id].js @@ -2,18 +2,20 @@ 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 { 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 ApiKeyTable from "~/components/basic/table/ApiKeyTable.tsx"; 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 AddApiKeyDialog from "../../../components/basic/dialog/AddApiKeyDialog"; import getUser from "../../api/user/getUser"; export default function PersonalSettings() { @@ -29,6 +31,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(); @@ -45,6 +49,10 @@ export default function PersonalSettings() { setPersonalName(user.firstName + " " + user.lastName); }, []); + const closeAddApiKeyModal = () => { + setIsAddApiKeyDialogOpen(false); + }; + return (
@@ -53,6 +61,13 @@ export default function PersonalSettings() { +
-
-
- {/*
-
-

- Display Name -

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

- Saved -

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

{t("settings-personal:change-language")} @@ -132,6 +99,44 @@ export default function PersonalSettings() { />

+
+
+
+

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

+

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

+

+ Please, make sure you are on the + + latest version of CLI + . +

+
+
+
+
+ +
@@ -295,7 +300,7 @@ export default function PersonalSettings() {
-
+

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" + } } From 61f767e89530b3b1f18b7514b0f86f512c5acdc1 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Thu, 12 Jan 2023 22:12:23 -0800 Subject: [PATCH 2/5] Corrected the telemetery event name --- backend/src/controllers/v2/secretsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index f92fc4cee..dc444492c 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -203,7 +203,7 @@ export const getSecrets = async (req: Request, res: Response) => { if (postHogClient) { postHogClient.capture({ - event: 'secrets added', + event: 'secrets pulled', distinctId: req.user.email, properties: { numberOfSecrets: secrets.length, From 74b76eda7e9b5831647e092b12fb50a0e06fd649 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 15 Jan 2023 14:25:23 +0700 Subject: [PATCH 3/5] Complete v1 API Key --- README.md | 7 +- backend/src/app.ts | 2 +- backend/src/ee/routes/v1/secret.ts | 4 +- backend/src/ee/routes/v1/workspace.ts | 6 +- backend/src/helpers/auth.ts | 79 +++++++----- backend/src/middleware/requireAuth.ts | 26 ++-- backend/src/routes/v2/secrets.ts | 8 +- backend/src/routes/v2/users.ts | 2 +- backend/src/routes/v2/workspace.ts | 8 +- .../basic/dialog/AddApiKeyDialog.js | 121 ++++++------------ .../components/basic/table/ApiKeyTable.tsx | 22 ++-- frontend/pages/api/apiKey/addAPIKey.ts | 37 ++++++ frontend/pages/api/apiKey/deleteAPIKey.ts | 30 +++++ frontend/pages/api/apiKey/getAPIKeys.ts | 26 ++++ .../pages/api/serviceToken/addServiceToken.ts | 2 +- frontend/pages/settings/personal/[id].js | 40 +++--- .../public/locales/en/section-api-key.json | 13 ++ 17 files changed, 250 insertions(+), 183 deletions(-) create mode 100644 frontend/pages/api/apiKey/addAPIKey.ts create mode 100644 frontend/pages/api/apiKey/deleteAPIKey.ts create mode 100644 frontend/pages/api/apiKey/getAPIKeys.ts create mode 100644 frontend/public/locales/en/section-api-key.json diff --git a/README.md b/README.md index 3a3860d2c..dd3710924 100644 --- a/README.md +++ b/README.md @@ -335,13 +335,8 @@ Infisical officially launched as v.1.0 on November 21st, 2022. There are a lot o - ## 🌎 Translations -<<<<<<< HEAD -Infisical is currently aviable in English and Korean. Help us translate Infisical to your language! -======= -Infisical is currently available in English and Korean. Help us translate Infisical to your language! ->>>>>>> 9ce4a52b8da0057c2450cd7af93a8c5758c2476b +Infisical is currently available in English and Korean. Help us translate Infisical to your language! You can find all the info in [this issue](https://github.com/Infisical/infisical/issues/181). 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 index 3c9c35938..5adfd7dfd 100644 --- a/frontend/components/basic/dialog/AddApiKeyDialog.js +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -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 == "" ? ( - {t("section-token:add-dialog.title", { + {t("section-api-key:add-dialog.title", { target: workspaceName, })}

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

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

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

@@ -215,19 +177,20 @@ const AddApiKeyDialog = ({
- {serviceToken} + {apiKey}
@@ -365,4 +366,5 @@ export const getServerSideProps = getTranslatedServerSideProps([ "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" + } +} From b8fbc36b2d4332475b15f2fa5248bbd897755cc3 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 15 Jan 2023 14:40:45 +0700 Subject: [PATCH 4/5] Fix faulty import --- frontend/components/basic/dialog/AddApiKeyDialog.js | 7 ------- frontend/pages/settings/personal/[id].tsx | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/components/basic/dialog/AddApiKeyDialog.js b/frontend/components/basic/dialog/AddApiKeyDialog.js index 618b8a65f..99bbbd9dc 100644 --- a/frontend/components/basic/dialog/AddApiKeyDialog.js +++ b/frontend/components/basic/dialog/AddApiKeyDialog.js @@ -6,13 +6,6 @@ import { Dialog, Transition } from "@headlessui/react"; 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, - encryptSymmetric, -} from "../../utilities/cryptography/crypto"; import Button from "../buttons/Button"; import InputField from "../InputField"; import ListBox from "../Listbox"; diff --git a/frontend/pages/settings/personal/[id].tsx b/frontend/pages/settings/personal/[id].tsx index 297e7a5e8..aed87bdc6 100644 --- a/frontend/pages/settings/personal/[id].tsx +++ b/frontend/pages/settings/personal/[id].tsx @@ -8,7 +8,7 @@ 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 ApiKeyTable from "~/components/basic/table/ApiKeyTable.tsx"; +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"; From 061a9c8583217bafeb19fa8cf5ab1a4e982ef6dd Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 15 Jan 2023 14:47:23 +0700 Subject: [PATCH 5/5] Fix build errors --- frontend/components/basic/table/ApiKeyTable.tsx | 1 - frontend/pages/settings/personal/[id].tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/components/basic/table/ApiKeyTable.tsx b/frontend/components/basic/table/ApiKeyTable.tsx index 1e9f29221..dd71b1b3f 100644 --- a/frontend/components/basic/table/ApiKeyTable.tsx +++ b/frontend/components/basic/table/ApiKeyTable.tsx @@ -9,7 +9,6 @@ import Button from '../buttons/Button'; interface TokenProps { _id: string; name: string; - environment: string; expiresAt: string; } diff --git a/frontend/pages/settings/personal/[id].tsx b/frontend/pages/settings/personal/[id].tsx index aed87bdc6..cd6428284 100644 --- a/frontend/pages/settings/personal/[id].tsx +++ b/frontend/pages/settings/personal/[id].tsx @@ -121,7 +121,7 @@ export default function PersonalSettings() {