Service tokens update on frontend

This commit is contained in:
Vladyslav Matsiiako
2023-01-04 18:54:45 -08:00
parent 6c88c4dc36
commit 9cf28fef5f
7 changed files with 107 additions and 80 deletions
@@ -190,7 +190,7 @@ export const getWorkspaceServiceTokenData = async (
) => { ) => {
let serviceTokenData; let serviceTokenData;
try { try {
const { workspaceId } = req.query; const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData serviceTokenData = await ServiceTokenData
.find({ .find({
@@ -12,6 +12,7 @@ import { envMapping } from "../../../public/data/frequentConstants";
import { import {
decryptAssymmetric, decryptAssymmetric,
encryptAssymmetric, encryptAssymmetric,
encryptSymmetric,
} from "../../utilities/cryptography/crypto"; } from "../../utilities/cryptography/crypto";
import Button from "../buttons/Button"; import Button from "../buttons/Button";
import InputField from "../InputField"; import InputField from "../InputField";
@@ -25,6 +26,8 @@ const expiryMapping = {
"12 months": 31104000, "12 months": 31104000,
}; };
const crypto = require('crypto');
const AddServiceTokenDialog = ({ const AddServiceTokenDialog = ({
isOpen, isOpen,
closeModal, closeModal,
@@ -48,16 +51,14 @@ const AddServiceTokenDialog = ({
privateKey: localStorage.getItem("PRIVATE_KEY"), privateKey: localStorage.getItem("PRIVATE_KEY"),
}); });
// generate new public/private key pair const randomBytes = crypto.randomBytes(16).toString('hex');
const pair = nacl.box.keyPair(); const {
const publicKey = nacl.util.encodeBase64(pair.publicKey); ciphertext,
const privateKey = nacl.util.encodeBase64(pair.secretKey); iv,
tag,
// encrypt workspace key under newly-generated public key } = encryptSymmetric({
const { ciphertext: encryptedKey, nonce } = encryptAssymmetric({
plaintext: key, plaintext: key,
publicKey, key: randomBytes,
privateKey,
}); });
let newServiceToken = await addServiceToken({ let newServiceToken = await addServiceToken({
@@ -65,13 +66,12 @@ const AddServiceTokenDialog = ({
workspaceId, workspaceId,
environment: envMapping[serviceTokenEnv], environment: envMapping[serviceTokenEnv],
expiresIn: expiryMapping[serviceTokenExpiresIn], expiresIn: expiryMapping[serviceTokenExpiresIn],
publicKey, encryptedKey: ciphertext,
encryptedKey, iv,
nonce, tag
}); });
const serviceToken = newServiceToken + "," + privateKey; setServiceToken(newServiceToken);
setServiceToken(serviceToken);
}; };
function copyToClipboard() { function copyToClipboard() {
@@ -161,7 +161,7 @@ const AddServiceTokenDialog = ({
"Production", "Production",
"Testing", "Testing",
]} ]}
width="full" isFull={true}
text={`${t("common:environment")}: `} text={`${t("common:environment")}: `}
/> />
</div> </div>
@@ -176,7 +176,7 @@ const AddServiceTokenDialog = ({
"6 months", "6 months",
"12 months", "12 months",
]} ]}
width="full" isFull={true}
text={`${t("common:expired-in")}: `} text={`${t("common:expired-in")}: `}
/> />
</div> </div>
@@ -211,7 +211,7 @@ const AddServiceTokenDialog = ({
</div> </div>
</div> </div>
<div className="w-full"> <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-44"> <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-14">
<input <input
type="text" type="text"
value={serviceToken} value={serviceToken}
@@ -236,7 +236,7 @@ const AddServiceTokenDialog = ({
)} )}
</button> </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"> <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")} {t("common:click-to-copy")}
</span> </span>
</div> </div>
</div> </div>
@@ -1,19 +1,37 @@
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { faX } from '@fortawesome/free-solid-svg-icons'; 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 { reverseEnvMapping } from '../../../public/data/frequentConstants';
import guidGenerator from '../../utilities/randomId'; import guidGenerator from '../../utilities/randomId';
import Button from '../buttons/Button'; import Button from '../buttons/Button';
interface TokenProps {
_id: string;
name: string;
environment: string;
expiresAt: string;
}
interface ServiceTokensProps {
data: TokenProps[];
workspaceName: string;
setServiceTokens: (value: TokenProps[]) => void;
}
/** /**
* This is the component that we utilize for the user table - in future, can reuse it for some other purposes too. * This is the component that we utilize for the service token table
* #TODO: add the possibility of choosing and doing operations on multiple users. * #TODO: add the possibility of choosing and doing operations on multiple users.
* @param {*} props * @param {object} obj
* @param {any[]} obj.data - current state of the service token table
* @param {string} obj.workspaceName - name of the current project
* @param {function} obj.setServiceTokens - updating the state of the service token table
* @returns * @returns
*/ */
const ServiceTokenTable = ({ data, workspaceName }) => { const ServiceTokenTable = ({ data, workspaceName, setServiceTokens }: ServiceTokensProps) => {
const router = useRouter(); console.log(data)
const { createNotification } = useNotificationContext();
return ( return (
<div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1"> <div className="table-container w-full bg-bunker rounded-md mb-6 border border-mineshaft-700 relative mt-1">
@@ -30,7 +48,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
</thead> </thead>
<tbody> <tbody>
{data?.length > 0 ? ( {data?.length > 0 ? (
data.map((row, index) => { data.map((row) => {
return ( return (
<tr <tr
key={guidGenerator()} key={guidGenerator()}
@@ -51,7 +69,14 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
<td className="py-2 border-mineshaft-700 border-t"> <td className="py-2 border-mineshaft-700 border-t">
<div className="opacity-50 hover:opacity-100 duration-200 flex items-center"> <div className="opacity-50 hover:opacity-100 duration-200 flex items-center">
<Button <Button
onButtonPressed={() => {}} onButtonPressed={() => {
deleteServiceToken({ serviceTokenId: row._id} );
setServiceTokens(data.filter(token => token._id != row._id));
createNotification({
text: `'${row.name}' token has been revoked.`,
type: 'error'
});
}}
color="red" color="red"
size="icon-sm" size="icon-sm"
icon={faX} icon={faX}
@@ -63,7 +88,7 @@ const ServiceTokenTable = ({ data, workspaceName }) => {
}) })
) : ( ) : (
<tr> <tr>
<td colSpan="4" className="text-center pt-7 pb-4 text-bunker-400"> <td className="text-center pt-7 pb-4 text-bunker-400 col-span-4">
No service tokens yet No service tokens yet
</td> </td>
</tr> </tr>
@@ -5,14 +5,21 @@ interface Props {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
expiresIn: number; expiresIn: number;
publicKey: string;
encryptedKey: string; encryptedKey: string;
nonce: string; iv: string;
tag: string;
} }
/** /**
* This route gets service tokens for a specific user in a project * This route gets service tokens for a specific user in a project
* @param {*} param0 * @param {object} obj
* @param {string} obj.name - name of the service token
* @param {string} obj.workspaceId - workspace for which we are issuing the token
* @param {string} obj.environment - environment for which we are issuing the token
* @param {string} obj.expiresIn - how soon the service token expires in ms
* @param {string} obj.encryptedKey - encrypted project key through random symmetric encryption
* @param {string} obj.iv - obtained through symmetric encryption
* @param {string} obj.tag - obtained through symmetric encryption
* @returns * @returns
*/ */
const addServiceToken = ({ const addServiceToken = ({
@@ -20,11 +27,11 @@ const addServiceToken = ({
workspaceId, workspaceId,
environment, environment,
expiresIn, expiresIn,
publicKey,
encryptedKey, encryptedKey,
nonce iv,
tag
}: Props) => { }: Props) => {
return SecurityClient.fetchCall('/api/v1/service-token/', { return SecurityClient.fetchCall('/api/v2/service-token-data/', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -34,13 +41,13 @@ const addServiceToken = ({
workspaceId, workspaceId,
environment, environment,
expiresIn, expiresIn,
publicKey,
encryptedKey, encryptedKey,
nonce iv,
tag
}) })
}).then(async (res) => { }).then(async (res) => {
if (res && res.status == 200) { if (res && res.status == 200) {
return (await res.json()).token; return (await res.json()).serviceToken;
} else { } else {
console.log('Failed to add service tokens'); console.log('Failed to add service tokens');
} }
@@ -0,0 +1,30 @@
import SecurityClient from '~/utilities/SecurityClient';
interface Props {
serviceTokenId: string;
}
/**
* This route revokes a specific service token
* @param {object} obj
* @param {string} obj.serviceTokenId - id of a cervice token that we want to delete
* @returns
*/
const deleteServiceToken = ({
serviceTokenId,
}: Props) => {
return SecurityClient.fetchCall('/api/v2/service-token-data/' + serviceTokenId, {
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 a service token');
}
});
};
export default deleteServiceToken;
@@ -7,7 +7,7 @@ import SecurityClient from '~/utilities/SecurityClient';
*/ */
const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => { const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => {
return SecurityClient.fetchCall( return SecurityClient.fetchCall(
'/api/v1/workspace/' + workspaceId + '/service-tokens', '/api/v2/workspace/' + workspaceId + '/service-token-data',
{ {
method: 'GET', method: 'GET',
headers: { headers: {
@@ -16,7 +16,7 @@ const getServiceTokens = ({ workspaceId }: { workspaceId: string }) => {
} }
).then(async (res) => { ).then(async (res) => {
if (res && res.status == 200) { if (res && res.status == 200) {
return (await res.json()).serviceTokens; return (await res.json()).serviceTokenData;
} else { } else {
console.log('Failed to get service tokens'); console.log('Failed to get service tokens');
} }
+6 -41
View File
@@ -8,7 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from "~/components/basic/buttons/Button"; import Button from "~/components/basic/buttons/Button";
import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog"; import AddServiceTokenDialog from "~/components/basic/dialog/AddServiceTokenDialog";
import InputField from "~/components/basic/InputField"; import InputField from "~/components/basic/InputField";
import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable"; import ServiceTokenTable from "~/components/basic/table/ServiceTokenTable.tsx";
import NavHeader from "~/components/navigation/NavHeader"; import NavHeader from "~/components/navigation/NavHeader";
import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps"; import { getTranslatedServerSideProps } from "~/utilities/withTranslateProps";
@@ -145,7 +145,7 @@ export default function SettingsBasic() {
<div className="flex flex-col"> <div className="flex flex-col">
<div className="min-w-md mt-2 flex flex-col items-start"> <div className="min-w-md mt-2 flex flex-col items-start">
<div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2"> <div className="bg-white/5 rounded-md px-6 pt-6 pb-4 flex flex-col items-start flex flex-col items-start w-full mb-6 pt-2">
<p className="text-xl font-semibold mb-4"> <p className="text-xl font-semibold mb-4 mt-2">
{t("common:display-name")} {t("common:display-name")}
</p> </p>
<div className="max-h-28 w-full max-w-md mr-auto"> <div className="max-h-28 w-full max-w-md mr-auto">
@@ -171,7 +171,7 @@ export default function SettingsBasic() {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4"> <div className="bg-white/5 rounded-md px-6 pt-4 pb-2 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4">
<p className="text-xl font-semibold self-start"> <p className="text-xl font-semibold self-start">
{t("common:project-id")} {t("common:project-id")}
</p> </p>
@@ -219,7 +219,7 @@ export default function SettingsBasic() {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-white/5 rounded-md px-6 pt-6 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2"> <div className="bg-white/5 rounded-md px-6 pt-4 flex flex-col items-start flex flex-col items-start w-full mt-4 mb-4 pt-2">
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<p className="text-xl font-semibold mb-3"> <p className="text-xl font-semibold mb-3">
@@ -244,47 +244,12 @@ export default function SettingsBasic() {
<ServiceTokenTable <ServiceTokenTable
data={serviceTokens} data={serviceTokens}
workspaceName={workspaceName} workspaceName={workspaceName}
setServiceTokens={setServiceTokens}
/> />
</div> </div>
{/* <div className="bg-white/5 rounded-md px-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-6 pt-6">
<p className="text-xl font-semibold self-start">
Project Environments
</p>
<p className="text-md mr-1 text-gray-400 mt-2 self-start">
Choose which environments will show up
in your Dashboard. Some common ones
include Development, Staging, and
Production. Often, teams choose to add
Testing.
</p>
<p className="text-sm mr-1 text-gray-500 self-start">
Note: the text in brackets shows how
these environmant should be accessed in
CLI.
</p>
<div className="rounded-md h-10 w-full mr-auto mt-4 flex flex-row">
{envOptions.map((env) => (
<div className="bg-white/5 hover:bg-white/10 duration-200 h-full w-max px-3 flex flex-row items-center justify-between rounded-md mr-1 text-sm">
{env}
<XIcon
className="h-5 w-5 ml-2 mt-0.5 text-white cursor-pointer"
aria-hidden="true"
/>
</div>
))}
<div className="group bg-white/5 hover:bg-primary hover:text-black duration-200 h-full w-max py-1 px-3 flex flex-row items-center justify-between rounded-md mr-1 cursor-pointer text-sm font-semibold">
<PlusIcon
className="h-5 w-5 text-white mr-2 group-hover:text-black"
aria-hidden="true"
/>
Add
</div>
</div>
</div> */}
</div> </div>
</div> </div>
<div className="bg-white/5 rounded-md px-6 pt-6 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2"> <div className="bg-white/5 rounded-md px-6 pt-4 pb-6 border-l border-red pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pb-4 pt-2">
<p className="text-xl font-bold text-red"> <p className="text-xl font-bold text-red">
{t("settings-project:danger-zone")} {t("settings-project:danger-zone")}
</p> </p>