mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 14:34:09 +00:00
Merge branch 'issue-318' of https://github.com/esau-morais/infisical into issue-318
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
clientProof: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
clientProof: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,26 +17,30 @@ interface Props {
|
||||
* @param {*} clientPublicKey
|
||||
* @returns
|
||||
*/
|
||||
const changePassword2 = ({ encryptedPrivateKey, iv, tag, salt, verifier, clientProof }: Props) =>
|
||||
SecurityClient.fetchCall('/api/v1/password/change-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientProof,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res;
|
||||
}
|
||||
console.log('Failed to change the password');
|
||||
return undefined;
|
||||
const changePassword2 = async ({
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post('/api/v1/password/change-password', {
|
||||
clientProof,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default changePassword2;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
ciphertext: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
organizationName: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,6 +24,9 @@ interface Props {
|
||||
* @param {string} obj.email - email of the user completing signup
|
||||
* @param {string} obj.firstName - first name of the user completing signup
|
||||
* @param {string} obj.lastName - last name of the user completing sign up
|
||||
* @param {string} obj.protectedKey - protected key in encryption version 2
|
||||
* @param {string} obj.protectedKeyIV - IV of protected key in encryption version 2
|
||||
* @param {string} obj.protectedKeyTag - tag of protected key in encryption version 2
|
||||
* @param {string} obj.organizationName - organization name for this user (usually, [FIRST_NAME]'s organization)
|
||||
* @param {string} obj.publicKey - public key of the user completing signup
|
||||
* @param {string} obj.ciphertext
|
||||
@@ -26,39 +34,40 @@ interface Props {
|
||||
* @param {string} obj.tag
|
||||
* @param {string} obj.salt
|
||||
* @param {string} obj.verifier
|
||||
* @param {string} obj.token - token that confirms a user's identity
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignup = ({
|
||||
const completeAccountInformationSignup = async ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => fetch('/api/v1/signup/complete-account/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${ token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
organizationName,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
organizationName
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post('/api/v2/signup/complete-account/signup', {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
organizationName
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default completeAccountInformationSignup;
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface Props {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
publicKey: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
encryptedPrivateKey: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
// missing token?
|
||||
// TODO: add to SecurityClient
|
||||
|
||||
|
||||
/**
|
||||
* This function is called in the end of the signup process.
|
||||
* It sends all the necessary nformation to the server.
|
||||
@@ -27,34 +35,36 @@ interface Props {
|
||||
* @param {string} obj.token - token that confirms a user's identity
|
||||
* @returns
|
||||
*/
|
||||
const completeAccountInformationSignupInvite = ({
|
||||
const completeAccountInformationSignupInvite = async ({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier,
|
||||
token
|
||||
}: Props) => fetch('/api/v1/signup/complete-account/invite', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${ token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
verifier
|
||||
}: Props) => {
|
||||
const { data } = await apiRequest.post('/api/v2/signup/complete-account/invite', {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default completeAccountInformationSignupInvite;
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Login1 {
|
||||
* @returns
|
||||
*/
|
||||
const login1 = async (email: string, clientPublicKey: string) => {
|
||||
const response = await fetch("/api/v1/auth/login1", {
|
||||
const response = await fetch("/api/v2/auth/login1", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -25,7 +25,7 @@ const login1 = async (email: string, clientPublicKey: string) => {
|
||||
const data = (await response.json()) as unknown as Login1;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
throw new Error("Wrong password");
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
interface Login2Response {
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
publicKey: string;
|
||||
tag: string;
|
||||
mfaEnabled: boolean;
|
||||
token: string;
|
||||
encryptionVersion?: number;
|
||||
protectedKey?: string;
|
||||
protectedKeyIV?: string;
|
||||
protectedKeyTag?: string;
|
||||
publicKey?: string;
|
||||
encryptedPrivateKey?: string;
|
||||
iv?: string;
|
||||
tag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13,7 +18,7 @@ interface Login2Response {
|
||||
* @returns
|
||||
*/
|
||||
const login2 = async (email: string, clientProof: string) => {
|
||||
const response = await fetch('/api/v1/auth/login2', {
|
||||
const response = await fetch('/api/v2/auth/login2', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@@ -4,27 +4,38 @@ import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
* This route logs the user out. Note: the user should authorized to do this.
|
||||
* We first try to log out - if the authorization fails (response.status = 401), we refetch the new token, and then retry
|
||||
*/
|
||||
const logout = async () =>
|
||||
SecurityClient.fetchCall('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
}).then((res) => {
|
||||
const logout = async () => {
|
||||
try {
|
||||
const res = await SecurityClient.fetchCall('/api/v1/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (res?.status === 200) {
|
||||
SecurityClient.setToken('');
|
||||
// Delete the cookie by not setting a value; Alternatively clear the local storage
|
||||
localStorage.setItem('publicKey', '');
|
||||
localStorage.setItem('encryptedPrivateKey', '');
|
||||
localStorage.setItem('iv', '');
|
||||
localStorage.setItem('tag', '');
|
||||
localStorage.setItem('PRIVATE_KEY', '');
|
||||
console.log('User logged out', res);
|
||||
localStorage.removeItem('protectedKey');
|
||||
localStorage.removeItem('protectedKeyIV');
|
||||
localStorage.removeItem('protectedKeyTag');
|
||||
localStorage.removeItem('publicKey');
|
||||
localStorage.removeItem('encryptedPrivateKey');
|
||||
localStorage.removeItem('iv');
|
||||
localStorage.removeItem('tag');
|
||||
localStorage.removeItem('PRIVATE_KEY');
|
||||
localStorage.removeItem('orgData.id');
|
||||
localStorage.removeItem('projectData.id');
|
||||
|
||||
return res;
|
||||
}
|
||||
console.log('Failed to log out');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error logging out', error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export default logout;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
interface Props {
|
||||
verificationToken: string;
|
||||
protectedKey: string;
|
||||
protectedKeyIV: string;
|
||||
protectedKeyTag: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
encryptedPrivateKeyIV: string;
|
||||
encryptedPrivateKeyTag: string;
|
||||
salt: string;
|
||||
verifier: string;
|
||||
verificationToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -19,22 +22,28 @@ interface Props {
|
||||
* @returns
|
||||
*/
|
||||
const resetPasswordOnAccountRecovery = ({
|
||||
verificationToken,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
verifier,
|
||||
verificationToken,
|
||||
}: Props) => fetch('/api/v1/password/password-reset', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${ verificationToken}`
|
||||
Authorization: `Bearer ${verificationToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
iv,
|
||||
tag,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt,
|
||||
verifier
|
||||
})
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
/**
|
||||
* Verify MFA token [mfaToken] for user with email [email]
|
||||
* @param {object} obj
|
||||
* @param {string} obj.email - email of user
|
||||
* @param {string} obj.mfaToken - MFA cod/token to verify
|
||||
* @returns
|
||||
*/
|
||||
const verifyMfaToken = async ({
|
||||
email,
|
||||
mfaToken
|
||||
}: {
|
||||
email: string;
|
||||
mfaToken: string;
|
||||
}) => {
|
||||
const { data } = await apiRequest.post('/api/v2/auth/mfa/verify', {
|
||||
email,
|
||||
mfaToken
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default verifyMfaToken;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
interface RequestType {
|
||||
method: string;
|
||||
secret: {
|
||||
type: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
tags: string[];
|
||||
}
|
||||
}
|
||||
|
||||
const batchSecrets = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
requests
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
requests: RequestType[];
|
||||
}) => {
|
||||
const { data } = await apiRequest.post('/api/v2/secrets/batch', {
|
||||
workspaceId,
|
||||
environment,
|
||||
requests
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export default batchSecrets;
|
||||
@@ -0,0 +1,32 @@
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
|
||||
interface Props {
|
||||
isMfaEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's MFA-enabled status to [isMfaEnabled]
|
||||
* @param {Object} obj
|
||||
* @param {Boolean} obj.isMfaEnabled - whether or not MFA status should be set to enabled or not
|
||||
* @returns {User} user - user with updated MFA-enabled status
|
||||
*/
|
||||
const updateMyMfaEnabled = async ({
|
||||
isMfaEnabled
|
||||
}: Props) =>
|
||||
SecurityClient.fetchCall(`/api/v2/users/me/mfa`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isMfaEnabled,
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).user;
|
||||
}
|
||||
console.log('Failed to update MFA status');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateMyMfaEnabled;
|
||||
@@ -30,6 +30,7 @@ import DropZone from '@app/components/dashboard/DropZone';
|
||||
import KeyPair from '@app/components/dashboard/KeyPair';
|
||||
import SideBar from '@app/components/dashboard/SideBar';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import { decryptAssymmetric, decryptSymmetric } from '@app/components/utilities/cryptography/crypto';
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
import encryptSecrets from '@app/components/utilities/secrets/encryptSecrets';
|
||||
import getSecretsForProject from '@app/components/utilities/secrets/getSecretsForProject';
|
||||
@@ -41,12 +42,14 @@ import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
|
||||
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
|
||||
import { useLeaveConfirm } from '@app/hooks';
|
||||
|
||||
import addSecrets from '../api/files/AddSecrets';
|
||||
import deleteSecrets from '../api/files/DeleteSecrets';
|
||||
import updateSecrets from '../api/files/UpdateSecrets';
|
||||
// import addSecrets from '../api/files/AddSecrets';
|
||||
// import deleteSecrets from '../api/files/DeleteSecrets';
|
||||
// import updateSecrets from '../api/files/UpdateSecrets';
|
||||
import batchSecrets from '../api/files/batchSecrets';
|
||||
import getUser from '../api/user/getUser';
|
||||
import checkUserAction from '../api/userActions/checkUserAction';
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
import getLatestFileKey from '../api/workspace/getLatestFileKey';
|
||||
import getWorkspaceEnvironments from '../api/workspace/getWorkspaceEnvironments';
|
||||
import getWorkspaces from '../api/workspace/getWorkspaces';
|
||||
import getWorkspaceTags from '../api/workspace/getWorkspaceTags';
|
||||
@@ -94,6 +97,32 @@ interface SnapshotProps {
|
||||
}[];
|
||||
}
|
||||
|
||||
interface EncryptedSecretProps {
|
||||
_id: string;
|
||||
createdAt: string;
|
||||
environment: string;
|
||||
secretCommentCiphertext: string;
|
||||
secretCommentIV: string;
|
||||
secretCommentTag: string;
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
type: 'personal' | 'shared';
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
interface SecretProps {
|
||||
key: string;
|
||||
value: string | undefined;
|
||||
type: 'personal' | 'shared';
|
||||
comment: string;
|
||||
id: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
/**
|
||||
* this function finds the teh duplicates in an array
|
||||
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
|
||||
@@ -188,6 +217,9 @@ export default function Dashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (router.isReady && workspaceId === 'undefined') {
|
||||
router.push('/noprojects');
|
||||
}
|
||||
try {
|
||||
const { push, query } = router
|
||||
const tempNumSnapshots = await getProjectSercetSnapshotsCount({
|
||||
@@ -306,7 +338,7 @@ export default function Dashboard() {
|
||||
valueOverride: undefined,
|
||||
comment: '',
|
||||
tags: []
|
||||
},
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -323,50 +355,50 @@ export default function Dashboard() {
|
||||
);
|
||||
};
|
||||
|
||||
const modifyValue = (value: string, pos: number) => {
|
||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, value } : e)));
|
||||
const modifyValue = (value: string, id: string) => {
|
||||
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, value } : e)));
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const modifyValueOverride = (valueOverride: string | undefined, pos: number) => {
|
||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, valueOverride } : e)));
|
||||
const modifyValueOverride = (valueOverride: string | undefined, id: string) => {
|
||||
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, valueOverride } : e)));
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const modifyKey = (key: string, pos: number) => {
|
||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, key } : e)));
|
||||
const modifyKey = (key: string, id: string) => {
|
||||
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, key } : e)));
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const modifyComment = (comment: string, pos: number) => {
|
||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, comment } : e)));
|
||||
const modifyComment = (comment: string, id: string) => {
|
||||
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, comment } : e)));
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
const modifyTags = (tags: Tag[], pos: number) => {
|
||||
setData((oldData) => oldData?.map((e) => (e.pos === pos ? { ...e, tags } : e)));
|
||||
const modifyTags = (tags: Tag[], id: string) => {
|
||||
setData((oldData) => oldData?.map((e) => ((e.id ? e.id : e.idOverride) === id ? { ...e, tags } : e)));
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
// For speed purposes and better perforamance, we are using useCallback
|
||||
const listenChangeValue = useCallback((value: string, pos: number) => {
|
||||
modifyValue(value, pos);
|
||||
const listenChangeValue = useCallback((value: string, id: string) => {
|
||||
modifyValue(value, id);
|
||||
}, []);
|
||||
|
||||
const listenChangeValueOverride = useCallback((value: string | undefined, pos: number) => {
|
||||
modifyValueOverride(value, pos);
|
||||
const listenChangeValueOverride = useCallback((value: string | undefined, id: string) => {
|
||||
modifyValueOverride(value, id);
|
||||
}, []);
|
||||
|
||||
const listenChangeKey = useCallback((value: string, pos: number) => {
|
||||
modifyKey(value, pos);
|
||||
const listenChangeKey = useCallback((value: string, id: string) => {
|
||||
modifyKey(value, id);
|
||||
}, []);
|
||||
|
||||
const listenChangeComment = useCallback((value: string, pos: number) => {
|
||||
modifyComment(value, pos);
|
||||
const listenChangeComment = useCallback((value: string, id: string) => {
|
||||
modifyComment(value, id);
|
||||
}, []);
|
||||
|
||||
const listenChangeTags = useCallback((value: Tag[], pos: number) => {
|
||||
modifyTags(value, pos);
|
||||
const listenChangeTags = useCallback((value: Tag[], id: string) => {
|
||||
modifyTags(value, id);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@@ -374,174 +406,335 @@ export default function Dashboard() {
|
||||
*/
|
||||
// TODO(akhilmhdh): split and make it small
|
||||
const savePush = async (dataToPush?: SecretDataProps[]) => {
|
||||
setSaveLoading(true);
|
||||
let newData: SecretDataProps[] | null | undefined;
|
||||
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
|
||||
if ((dataToPush ?? [])?.length > 0) {
|
||||
newData = dataToPush;
|
||||
} else {
|
||||
newData = data;
|
||||
}
|
||||
try {
|
||||
setSaveLoading(true);
|
||||
let newData: SecretDataProps[] | null | undefined;
|
||||
// dataToPush is mostly used for rollbacks, otherwise we always take the current state data
|
||||
if ((dataToPush ?? [])?.length > 0) {
|
||||
newData = dataToPush;
|
||||
} else {
|
||||
newData = data;
|
||||
}
|
||||
|
||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||
const nameErrors = !newData!
|
||||
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
|
||||
.every((v) => v === false);
|
||||
const duplicatesExist =
|
||||
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
||||
// Checking if any of the secret keys start with a number - if so, don't do anything
|
||||
const nameErrors = !newData!
|
||||
.map((secret) => !Number.isNaN(Number(secret.key.charAt(0))))
|
||||
.every((v) => v === false);
|
||||
const duplicatesExist =
|
||||
findDuplicates(data!.map((item: SecretDataProps) => item.key)).length > 0;
|
||||
|
||||
if (nameErrors) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
if (nameErrors) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Solve all name errors before saving secrets.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (duplicatesExist) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
if (duplicatesExist) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'Remove duplicated secret names before saving.',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedEnv?.isWriteDenied) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'You are not allowed to edit this environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
if (selectedEnv?.isWriteDenied) {
|
||||
setSaveLoading(false);
|
||||
return createNotification({
|
||||
text: 'You are not allowed to edit this environment',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Once "Save changes" is clicked, disable that button
|
||||
setHasUnsavedChanges(false);
|
||||
// Once "Save changes" is clicked, disable that button
|
||||
setHasUnsavedChanges(false);
|
||||
|
||||
const secretsToBeDeleted = initialData!
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
!newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
|
||||
)
|
||||
.map((secret) => secret.id);
|
||||
console.log('delete', secretsToBeDeleted.length);
|
||||
|
||||
const secretsToBeAdded = newData!.filter(
|
||||
(newDataPoint) =>
|
||||
!initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
|
||||
);
|
||||
console.log('add', secretsToBeAdded.length);
|
||||
|
||||
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
|
||||
initialData!
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
newData!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
||||
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
|
||||
initDataPoint.value ||
|
||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
||||
initDataPoint.key ||
|
||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
|
||||
initDataPoint.comment) ||
|
||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !==
|
||||
initDataPoint?.tags
|
||||
)
|
||||
.map((secret) => secret.id)
|
||||
.includes(newDataPoint.id)
|
||||
);
|
||||
console.log('update', secretsToBeUpdated.length);
|
||||
|
||||
const newOverrides = newData!.filter(
|
||||
(newDataPoint) => newDataPoint.valueOverride !== undefined
|
||||
);
|
||||
const initOverrides = initialData!.filter(
|
||||
(initDataPoint) => initDataPoint.valueOverride !== undefined
|
||||
);
|
||||
|
||||
const overridesToBeDeleted = initOverrides
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
|
||||
)
|
||||
.map((secret) => String(secret.idOverride));
|
||||
console.log('override delete', overridesToBeDeleted.length);
|
||||
|
||||
const overridesToBeAdded = newOverrides!
|
||||
.filter(
|
||||
(newDataPoint) =>
|
||||
!initOverrides.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
|
||||
)
|
||||
.map((override) => ({
|
||||
pos: override.pos,
|
||||
key: override.key,
|
||||
value: String(override.valueOverride),
|
||||
valueOverride: override.valueOverride,
|
||||
comment: '',
|
||||
id: String(override.idOverride),
|
||||
idOverride: String(override.idOverride),
|
||||
tags: override.tags
|
||||
}));
|
||||
console.log('override add', overridesToBeAdded.length);
|
||||
|
||||
const overridesToBeUpdated = newOverrides!
|
||||
.filter((newDataPoint) =>
|
||||
initOverrides
|
||||
const secretsToBeDeleted = initialData!
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
newOverrides!.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
||||
(newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
|
||||
.valueOverride !== initDataPoint.valueOverride ||
|
||||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
||||
initDataPoint.key ||
|
||||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]
|
||||
.comment !== initDataPoint.comment ||
|
||||
newOverrides!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags !== initDataPoint?.tags)
|
||||
!newData!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
|
||||
)
|
||||
.map((secret) => secret.id)
|
||||
.includes(newDataPoint.id)
|
||||
)
|
||||
.map((override) => ({
|
||||
pos: override.pos,
|
||||
key: override.key,
|
||||
value: String(override.valueOverride),
|
||||
valueOverride: override.valueOverride,
|
||||
comment: '',
|
||||
id: String(override.idOverride),
|
||||
idOverride: String(override.idOverride),
|
||||
tags: override.tags
|
||||
}));
|
||||
console.log('override update', overridesToBeUpdated.length);
|
||||
.map((secret) => secret.id);
|
||||
console.log('delete', secretsToBeDeleted.length);
|
||||
|
||||
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
|
||||
await deleteSecrets({ secretIds: secretsToBeDeleted.concat(overridesToBeDeleted) });
|
||||
}
|
||||
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
|
||||
workspaceId,
|
||||
env: selectedEnv.slug
|
||||
});
|
||||
if (secrets) await addSecrets({ secrets, env: selectedEnv.slug, workspaceId });
|
||||
}
|
||||
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
|
||||
workspaceId,
|
||||
env: selectedEnv.slug
|
||||
});
|
||||
if (secrets) await updateSecrets({ secrets });
|
||||
}
|
||||
const secretsToBeAdded = newData!.filter(
|
||||
(newDataPoint) =>
|
||||
!initialData!.map((initDataPoint) => initDataPoint.id).includes(newDataPoint.id)
|
||||
);
|
||||
console.log('add', secretsToBeAdded.length);
|
||||
|
||||
setInitialData(structuredClone(newData));
|
||||
const secretsToBeUpdated = newData!.filter((newDataPoint) =>
|
||||
initialData!
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
newData!.filter((dataPoint) => dataPoint.id)
|
||||
.map((dataPoint) => dataPoint.id).includes(initDataPoint.id) &&
|
||||
(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].value !==
|
||||
initDataPoint.value ||
|
||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].key !==
|
||||
initDataPoint.key ||
|
||||
newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0].comment !==
|
||||
initDataPoint.comment ||
|
||||
JSON.stringify(newData!.filter((dataPoint) => dataPoint.id === initDataPoint.id)[0]?.tags) !==
|
||||
JSON.stringify(initDataPoint?.tags))
|
||||
)
|
||||
.map((secret) => secret.id)
|
||||
.includes(newDataPoint.id)
|
||||
);
|
||||
console.log('update', secretsToBeUpdated.length);
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
setCheckDocsPopUpVisible(true);
|
||||
await registerUserAction({ action: 'first_time_secrets_pushed' });
|
||||
}
|
||||
const newOverrides = newData!.filter(
|
||||
(newDataPoint) => newDataPoint.valueOverride !== undefined
|
||||
);
|
||||
const initOverrides = initialData!.filter(
|
||||
(initDataPoint) => initDataPoint.valueOverride !== undefined
|
||||
);
|
||||
|
||||
// increasing the number of project commits
|
||||
setNumSnapshots((numSnapshots ?? 0) + 1);
|
||||
setSaveLoading(false);
|
||||
const overridesToBeDeleted = initOverrides
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
!newOverrides!.map((newDataPoint) => newDataPoint.id).includes(initDataPoint.id)
|
||||
)
|
||||
.map((secret) => String(secret.idOverride));
|
||||
console.log('override delete', overridesToBeDeleted.length);
|
||||
|
||||
const overridesToBeAdded = newOverrides!
|
||||
.filter(
|
||||
(newDataPoint) =>
|
||||
!initOverrides.map((initDataPoint) => initDataPoint.idOverride).includes(newDataPoint.idOverride)
|
||||
)
|
||||
.map((override) => ({
|
||||
pos: override.pos,
|
||||
key: override.key,
|
||||
value: String(override.valueOverride),
|
||||
valueOverride: override.valueOverride,
|
||||
comment: '',
|
||||
id: String(override.idOverride),
|
||||
idOverride: String(override.idOverride),
|
||||
tags: override.tags
|
||||
}));
|
||||
console.log('override add', overridesToBeAdded.length);
|
||||
|
||||
const overridesToBeUpdated = newOverrides!
|
||||
.filter((newDataPoint) =>
|
||||
initOverrides
|
||||
.filter(
|
||||
(initDataPoint) =>
|
||||
newOverrides!.map((dataPoint) => dataPoint.idOverride)
|
||||
.includes(initDataPoint.idOverride) &&
|
||||
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
|
||||
.valueOverride !== initDataPoint.valueOverride ||
|
||||
newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0].key !==
|
||||
initDataPoint.key ||
|
||||
(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]
|
||||
.comment || '') !== (initDataPoint.comment || '')
|
||||
||
|
||||
(JSON.stringify(newOverrides!.filter((dataPoint) => dataPoint.idOverride === initDataPoint.idOverride)[0]?.tags) || '') !==
|
||||
(JSON.stringify(initDataPoint?.tags) || '')
|
||||
)
|
||||
)
|
||||
.map((secret) => secret.idOverride)
|
||||
.includes(newDataPoint.idOverride)
|
||||
)
|
||||
.map((override) => ({
|
||||
pos: override.pos,
|
||||
key: override.key,
|
||||
value: String(override.valueOverride),
|
||||
valueOverride: override.valueOverride,
|
||||
comment: '',
|
||||
id: String(override.idOverride),
|
||||
idOverride: String(override.idOverride),
|
||||
tags: override.tags
|
||||
}));
|
||||
console.log('override update', overridesToBeUpdated.length);
|
||||
|
||||
const requests: any = []; // TODO: fix any
|
||||
if (secretsToBeDeleted.concat(overridesToBeDeleted).length > 0) {
|
||||
secretsToBeDeleted.concat(overridesToBeDeleted).forEach((_id: string) => {
|
||||
requests.push({
|
||||
method: 'DELETE',
|
||||
secret: {
|
||||
_id
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (selectedEnv && secretsToBeAdded.concat(overridesToBeAdded).length > 0) {
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeAdded.concat(overridesToBeAdded),
|
||||
workspaceId,
|
||||
env: selectedEnv.slug
|
||||
});
|
||||
if (secrets) {
|
||||
secrets.forEach((secret) => {
|
||||
requests.push({
|
||||
method: 'POST',
|
||||
secret: {
|
||||
type: secret.type,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
secretCommentCiphertext: secret.secretCommentCiphertext,
|
||||
secretCommentIV: secret.secretCommentIV,
|
||||
secretCommentTag: secret.secretCommentTag,
|
||||
tags: secret.tags
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
if (selectedEnv && !selectedEnv.isReadDenied && secretsToBeUpdated.concat(overridesToBeUpdated).length > 0) {
|
||||
const secrets = await encryptSecrets({
|
||||
secretsToEncrypt: secretsToBeUpdated.concat(overridesToBeUpdated),
|
||||
workspaceId,
|
||||
env: selectedEnv.slug
|
||||
});
|
||||
if (secrets) {
|
||||
secrets.forEach((secret) => {
|
||||
requests.push({
|
||||
method: 'PATCH',
|
||||
secret: {
|
||||
_id: secret.id,
|
||||
type: secret.type,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
secretCommentCiphertext: secret.secretCommentCiphertext,
|
||||
secretCommentIV: secret.secretCommentIV,
|
||||
secretCommentTag: secret.secretCommentTag,
|
||||
tags: secret.tags
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let newSecrets;
|
||||
if (selectedEnv && requests.length > 0) {
|
||||
newSecrets = await batchSecrets({
|
||||
workspaceId,
|
||||
environment: selectedEnv.slug,
|
||||
requests
|
||||
});
|
||||
}
|
||||
|
||||
let formattedNewDecryptedKeys;
|
||||
if (newSecrets.createdSecrets) {
|
||||
const latestKey = await getLatestFileKey({ workspaceId });
|
||||
|
||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||
|
||||
const tempDecryptedSecrets: SecretProps[] = [];
|
||||
if (latestKey) {
|
||||
// assymmetrically decrypt symmetric key with local private key
|
||||
const key = decryptAssymmetric({
|
||||
ciphertext: latestKey.latestKey.encryptedKey,
|
||||
nonce: latestKey.latestKey.nonce,
|
||||
publicKey: latestKey.latestKey.sender.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
// decrypt secret keys, values, and comments
|
||||
newSecrets.createdSecrets.forEach((secret: EncryptedSecretProps) => {
|
||||
const plainTextKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
let plainTextValue;
|
||||
if (secret.secretValueCiphertext !== undefined) {
|
||||
plainTextValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextValue = undefined;
|
||||
}
|
||||
|
||||
let plainTextComment;
|
||||
if (secret.secretCommentCiphertext) {
|
||||
plainTextComment = decryptSymmetric({
|
||||
ciphertext: secret.secretCommentCiphertext,
|
||||
iv: secret.secretCommentIV,
|
||||
tag: secret.secretCommentTag,
|
||||
key
|
||||
});
|
||||
} else {
|
||||
plainTextComment = '';
|
||||
}
|
||||
|
||||
tempDecryptedSecrets.push({
|
||||
id: secret._id,
|
||||
key: plainTextKey,
|
||||
value: plainTextValue,
|
||||
type: secret.type,
|
||||
comment: plainTextComment,
|
||||
tags: secret.tags
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const secretKeys = [...new Set(tempDecryptedSecrets.map((secret) => secret.key))];
|
||||
|
||||
formattedNewDecryptedKeys = secretKeys.map((key, index) => ({
|
||||
id: tempDecryptedSecrets.filter((secret) => secret.key === key && secret.type === 'shared')[0]
|
||||
?.id,
|
||||
idOverride: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.id,
|
||||
pos: (newData?.filter(dp => !dp.id?.includes('-'))?.length ?? 0) + index,
|
||||
key,
|
||||
value: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.value,
|
||||
valueOverride: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'personal'
|
||||
)[0]?.value,
|
||||
comment: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.comment,
|
||||
tags: tempDecryptedSecrets.filter(
|
||||
(secret) => secret.key === key && secret.type === 'shared'
|
||||
)[0]?.tags
|
||||
}));
|
||||
|
||||
setInitialData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))));
|
||||
setData(structuredClone(newData?.filter(dp => !dp.id?.includes('-')).concat(formattedNewDecryptedKeys.filter(dk => dk.id))))
|
||||
} else {
|
||||
setInitialData(structuredClone(newData));
|
||||
}
|
||||
|
||||
// If this user has never saved environment variables before, show them a prompt to read docs
|
||||
if (!hasUserEverPushed) {
|
||||
setCheckDocsPopUpVisible(true);
|
||||
await registerUserAction({ action: 'first_time_secrets_pushed' });
|
||||
}
|
||||
|
||||
// increasing the number of project commits
|
||||
setNumSnapshots((numSnapshots ?? 0) + 1);
|
||||
setSaveLoading(false);
|
||||
createNotification({
|
||||
text: `Successfully saved secrets.`,
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Something went wrong while saving secrets: ", error)
|
||||
createNotification({
|
||||
text: `Something went wrong while saving secrets.`,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -585,7 +778,9 @@ export default function Dashboard() {
|
||||
if (!window.confirm(leaveConfirmDefaultMessage)) return;
|
||||
}
|
||||
|
||||
const selectedWorkspaceEnv = workspaceEnvs.find(({ name }: { name: string }) => envName === name) || {
|
||||
const selectedWorkspaceEnv = workspaceEnvs.find(
|
||||
({ name }: { name: string }) => envName === name
|
||||
) || {
|
||||
name: 'unknown',
|
||||
slug: 'unknown',
|
||||
isWriteDenied: false,
|
||||
@@ -678,16 +873,18 @@ export default function Dashboard() {
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="flex justify-start max-w-sm mt-1 mr-2">
|
||||
{!selectedEnv?.isReadDenied && <Button
|
||||
text={String(`${numSnapshots} ${t('Commits')}`)}
|
||||
onButtonPressed={() => {
|
||||
toggleSidebar('None');
|
||||
togglePITSidebar(true)
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faClockRotateLeft}
|
||||
/>}
|
||||
{!selectedEnv?.isReadDenied && (
|
||||
<Button
|
||||
text={String(`${numSnapshots} ${t('Commits')}`)}
|
||||
onButtonPressed={() => {
|
||||
toggleSidebar('None');
|
||||
togglePITSidebar(true);
|
||||
}}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faClockRotateLeft}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{(data?.length !== 0 || hasUnsavedChanges) && !snapshotData && (
|
||||
<div className="flex justify-start max-w-sm mt-1">
|
||||
@@ -721,17 +918,24 @@ export default function Dashboard() {
|
||||
comment: '',
|
||||
tags: sv.tags
|
||||
}));
|
||||
setData(rolledBackSecrets);
|
||||
|
||||
// Perform the rollback globally
|
||||
performSecretRollback({ workspaceId, version: snapshotData.version });
|
||||
|
||||
setSnapshotData(undefined);
|
||||
createNotification({
|
||||
text: `Rollback has been performed successfully.`,
|
||||
type: 'success'
|
||||
});
|
||||
setHasUnsavedChanges(false);
|
||||
const result = await performSecretRollback({ workspaceId, version: snapshotData.version });
|
||||
if (result === undefined) {
|
||||
createNotification({
|
||||
text: `Something went wrong during the rollback.`,
|
||||
type: 'error'
|
||||
});
|
||||
} else {
|
||||
setData(rolledBackSecrets);
|
||||
createNotification({
|
||||
text: `Successfully rolled back secrets.`,
|
||||
type: 'success'
|
||||
});
|
||||
setSnapshotData(undefined);
|
||||
setHasUnsavedChanges(false);
|
||||
togglePITSidebar(false);
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
size="md"
|
||||
@@ -814,16 +1018,14 @@ export default function Dashboard() {
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
) : data?.length !== 0 ? (
|
||||
) : (data?.length !== 0 || snapshotData?.secretVersions) ? (
|
||||
<div className="flex flex-col w-full mt-1">
|
||||
<div
|
||||
onScroll={onSecretsAreaScroll}
|
||||
className="mt-1 max-h-[calc(100vh-280px)] overflow-hidden overflow-y-scroll no-scrollbar no-scrollbar::-webkit-scrollbar border border-mineshaft-600 rounded-md"
|
||||
>
|
||||
<div ref={secretsTop} />
|
||||
<div
|
||||
className='group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]'
|
||||
>
|
||||
<div className="group flex flex-col items-center bg-mineshaft-800 border-b-2 border-mineshaft-500 duration-100 sticky top-0 z-[60]">
|
||||
<div className="relative flex flex-row justify-between w-full mr-auto max-h-14 items-center">
|
||||
<div className="w-1/5 border-r border-mineshaft-600 flex flex-row items-center">
|
||||
<div className='text-transparent text-xs flex items-center justify-center w-12 h-10 cursor-default'>0</div>
|
||||
@@ -838,33 +1040,38 @@ export default function Dashboard() {
|
||||
</IconButton>}
|
||||
</div>
|
||||
<div className="w-5/12 border-r border-mineshaft-600">
|
||||
<div
|
||||
className='flex items-center rounded-lg mt-4 md:mt-0 max-h-10'
|
||||
>
|
||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12'>Value</div>
|
||||
<div className="flex items-center rounded-lg mt-4 md:mt-0 max-h-10">
|
||||
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-7/12">
|
||||
Value
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-[calc(10%)] border-r border-mineshaft-600">
|
||||
<div className="flex items-center max-h-16">
|
||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Comment</div>
|
||||
<div className="flex items-center max-h-16 overflow-hidden">
|
||||
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
|
||||
Comment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-2/12">
|
||||
<div className="flex items-center max-h-16">
|
||||
<div className='text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12'>Tags</div>
|
||||
<div className="text-bunker-300 px-2 font-semibold h-10 flex items-center w-3/12">
|
||||
Tags
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center"
|
||||
/>
|
||||
<div className='w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center'>
|
||||
<div className="w-[1.5rem] h-[2.35rem] ml-auto rounded-md flex flex-row justify-center items-center" />
|
||||
<div className="w-[1.5rem] h-[2.35rem] mr-2 flex items-center justfy-center">
|
||||
<div
|
||||
onKeyDown={() => null}
|
||||
role="none"
|
||||
onClick={() => { }}
|
||||
className="invisible group-hover:visible"
|
||||
>
|
||||
<FontAwesomeIcon className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible" icon={faXmark} />
|
||||
<FontAwesomeIcon
|
||||
className="text-bunker-300 hover:text-red pl-2 pr-6 text-lg mt-0.5 invisible"
|
||||
icon={faXmark}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -877,7 +1084,7 @@ export default function Dashboard() {
|
||||
|| row.tags?.map(tag => tag.name).join(" ")?.toUpperCase().includes(searchKeys.toUpperCase())
|
||||
|| row.comment?.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter((row) => !sharedToHide.includes(row.id))
|
||||
.filter((row) => row.value !== undefined)
|
||||
// .filter((row) => row.value !== undefined)
|
||||
.map((keyPair) => (
|
||||
<KeyPair
|
||||
isCapitalized={autoCapitalization}
|
||||
@@ -905,22 +1112,6 @@ export default function Dashboard() {
|
||||
?.sort((a, b) => a.key.localeCompare(b.key))
|
||||
.filter((row) => row.environment === selectedSnapshotEnv?.slug)
|
||||
.filter((row) => row.key.toUpperCase().includes(searchKeys.toUpperCase()))
|
||||
.filter(
|
||||
(row) =>
|
||||
!snapshotData.secretVersions
|
||||
?.filter((secretVersion) =>
|
||||
snapshotData.secretVersions
|
||||
?.map((item) => item.key)
|
||||
.filter(
|
||||
(item, index) =>
|
||||
index !==
|
||||
snapshotData.secretVersions?.map((i) => i.key).indexOf(item)
|
||||
)
|
||||
.includes(secretVersion.key)
|
||||
)
|
||||
?.map((item) => item.id)
|
||||
.includes(row.id)
|
||||
)
|
||||
.map((keyPair) => (
|
||||
<KeyPair
|
||||
isCapitalized={autoCapitalization}
|
||||
@@ -941,15 +1132,15 @@ export default function Dashboard() {
|
||||
tags={projectTags}
|
||||
/>
|
||||
))}
|
||||
<div className='bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]'>
|
||||
<div className='w-10' />
|
||||
<div className="bg-mineshaft-800 text-sm rounded-t-md hover:bg-mineshaft-700 h-10 w-full flex flex-row items-center border-b-2 border-mineshaft-500 sticky top-0 z-[60]">
|
||||
<div className="w-10" />
|
||||
<button
|
||||
type="button"
|
||||
className='text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer'
|
||||
className="text-bunker-300 relative font-normal h-10 flex items-center w-full cursor-pointer"
|
||||
onClick={addRowToBottom}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className='mr-3' />
|
||||
<span className='text-sm'>Add Secret</span>
|
||||
<FontAwesomeIcon icon={faPlus} className="mr-3" />
|
||||
<span className="text-sm">Add Secret</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -996,8 +1187,7 @@ export default function Dashboard() {
|
||||
<SideBar
|
||||
toggleSidebar={toggleSidebar}
|
||||
data={data.filter(
|
||||
(row: SecretDataProps) =>
|
||||
row.id === sidebarSecretId && row.value !== undefined
|
||||
(row: SecretDataProps) => row.id === sidebarSecretId && row.value !== undefined
|
||||
)}
|
||||
modifyKey={listenChangeKey}
|
||||
modifyValue={listenChangeValue}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconProp } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faSlack } from '@fortawesome/free-brands-svg-icons';
|
||||
@@ -16,6 +17,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import onboardingCheck from '@app/components/utilities/checks/OnboardingCheck';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { TabsObject } from '@app/components/v2/Tabs';
|
||||
|
||||
import registerUserAction from '../api/userActions/registerUserAction';
|
||||
|
||||
@@ -43,7 +45,7 @@ const learningItem = ({
|
||||
<a
|
||||
target={`${link.includes('https') ? '_blank' : '_self'}`}
|
||||
rel="noopener noreferrer"
|
||||
className="w-full"
|
||||
className={`w-full ${complete && 'opacity-30 hover:opacity-100 duration-200'}`}
|
||||
href={link}
|
||||
>
|
||||
<div
|
||||
@@ -57,13 +59,13 @@ const learningItem = ({
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="relative bg-bunker-700 hover:bg-bunker-500 shadow-xl duration-200 rounded-md border border-dashed border-bunker-400 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden my-1.5 cursor-pointer"
|
||||
className="relative group bg-bunker-500 hover:bg-mineshaft-700 shadow-xl duration-200 rounded-md border border-mineshaft-600 pl-2 pr-6 py-2 h-[5.5rem] w-full flex items-center justify-between overflow-hidden mb-3 cursor-pointer"
|
||||
>
|
||||
<div className="flex flex-row items-center mr-4">
|
||||
<FontAwesomeIcon icon={icon} className="text-4xl mx-2 w-16" />
|
||||
{complete && (
|
||||
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
|
||||
<div className="bg-bunker-500 group-hover:bg-mineshaft-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col items-start">
|
||||
@@ -72,11 +74,11 @@ const learningItem = ({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}
|
||||
className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-primary'}`}
|
||||
>
|
||||
{complete ? 'Complete!' : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
@@ -101,7 +103,7 @@ const learningItem = ({
|
||||
<div className="bg-bunker-700 w-7 h-7 rounded-full absolute left-11 top-10">
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="absolute text-4xl left-12 top-16 w-5 h-5 text-green"
|
||||
className="absolute text-4xl left-12 top-16 w-5 h-5 text-primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -110,10 +112,10 @@ const learningItem = ({
|
||||
<div className="text-sm font-normal mt-0.5">{subText}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-green'}`}>
|
||||
<div className={`pr-4 font-semibold text-sm w-28 text-right ${complete && 'text-primary'}`}>
|
||||
{complete ? 'Complete!' : `About ${time}`}
|
||||
</div>
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
{complete && <div className="absolute bottom-0 left-0 h-1 w-full bg-primary" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -142,16 +144,17 @@ export default function Home() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx-6 lg:mx-0 w-full overflow-y-scroll pt-4">
|
||||
<div className="mx-6 lg:mx-0 w-full pt-4">
|
||||
<Head>
|
||||
<title>Infisical Guide</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center text-gray-300 text-lg mx-auto max-w-2xl lg:max-w-3xl xl:max-w-4xl py-6">
|
||||
<div className="text-3xl font-bold text-left w-full">Your quick start guide</div>
|
||||
<div className="text-md text-left w-full pt-2 pb-4 text-bunker-300">
|
||||
<div className="flex relative flex-col items-center text-gray-300 text-lg mx-auto px-6 max-w-3xl lg:max-w-4xl xl:max-w-5xl py-6">
|
||||
<div className="text-5xl font-bold text-left w-full mt-12">Your quick start guide</div>
|
||||
<div className="text-lg text-left w-full pt-2 pb-4 mb-14 text-bunker-300">
|
||||
Click on the items below and follow the instructions.
|
||||
</div>
|
||||
<div className='absolute h-min top-0 right-0 hidden lg:block'><Image src="/images/dragon-book.svg" height={250} width={400} alt="start guise dragon illustration" /></div>
|
||||
{learningItem({
|
||||
text: 'Get to know Infisical',
|
||||
subText: '',
|
||||
@@ -170,17 +173,32 @@ export default function Home() {
|
||||
userAction: 'first_time_secrets_pushed',
|
||||
link: `/dashboard/${router.query.id}`
|
||||
})}
|
||||
{learningItem({
|
||||
text: 'Inject secrets locally',
|
||||
subText: 'Replace .env files with a more secure and efficient alternative.',
|
||||
complete: false,
|
||||
icon: faNetworkWired,
|
||||
time: '8 min',
|
||||
link: 'https://infisical.com/docs/getting-started/quickstart'
|
||||
})}
|
||||
<div className="relative group bg-bunker-500 shadow-xl duration-200 rounded-md border border-mineshaft-600 pl-2 pr-2 pt-4 pb-2 h-full w-full flex flex-col items-center justify-between overflow-hidden mb-3 cursor-default">
|
||||
<div className='w-full flex flex-row items-center mb-4 pr-4'>
|
||||
<div className="flex flex-row items-center mr-4 w-full">
|
||||
<FontAwesomeIcon icon={faNetworkWired} className="text-4xl mx-2 w-16" />
|
||||
{false && (
|
||||
<div className="bg-bunker-500 group-hover:bg-mineshaft-700 w-7 h-7 rounded-full absolute left-12 top-10 p-2 flex items-center justify-center">
|
||||
<FontAwesomeIcon icon={faCheckCircle} className="text-4xl w-5 h-5 text-green" />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex pl-0.5 flex-col items-start">
|
||||
<div className="text-xl font-semibold mt-0.5">Inject secrets locally</div>
|
||||
<div className="text-sm font-normal">Replace .env files with a more secure and efficient alternative.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`pr-4 font-semibold text-sm w-28 text-right ${false && 'text-green'}`}
|
||||
>
|
||||
About 2 min
|
||||
</div>
|
||||
</div>
|
||||
<TabsObject/>
|
||||
{false && <div className="absolute bottom-0 left-0 h-1 w-full bg-green" />}
|
||||
</div>
|
||||
{learningItem({
|
||||
text: 'Integrate Infisical with your infrastructure',
|
||||
subText: 'Only a few integrations are currently available. Many more coming soon!',
|
||||
subText: 'Connect Infisical to various 3rd party services and platforms.',
|
||||
complete: false,
|
||||
icon: faPlug,
|
||||
time: '15 min',
|
||||
|
||||
+31
-104
@@ -4,28 +4,23 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faWarning } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import Error from '@app/components/basic/Error';
|
||||
import InputField from '@app/components/basic/InputField';
|
||||
import ListBox from '@app/components/basic/Listbox';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
import LoginStep from '@app/components/login/LoginStep';
|
||||
import MFAStep from '@app/components/login/MFAStep';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { isLoggedIn } from '@app/reactQuery';
|
||||
|
||||
import getWorkspaces from './api/workspace/getWorkspaces';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [errorLogin, setErrorLogin] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState(1);
|
||||
const { t } = useTranslation();
|
||||
const lang = router.locale ?? 'en';
|
||||
|
||||
|
||||
const setLanguage = async (to: string) => {
|
||||
router.push('/login', '/login', { locale: to });
|
||||
@@ -45,29 +40,34 @@ export default function Login() {
|
||||
}
|
||||
};
|
||||
if (isLoggedIn()) {
|
||||
setIsAlreadyLoggedIn(true);
|
||||
redirectToDashboard();
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* This function check if the user entered the correct credentials and should be allowed to log in.
|
||||
*/
|
||||
const loginCheck = async () => {
|
||||
if (!email || !password) {
|
||||
return;
|
||||
|
||||
const renderStep = (loginStep: number) => {
|
||||
// TODO: add MFA step
|
||||
switch (loginStep) {
|
||||
case 1:
|
||||
return (
|
||||
<LoginStep
|
||||
email={email}
|
||||
setEmail={setEmail}
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setStep={setStep}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
// TODO: add MFA step
|
||||
return (
|
||||
<MFAStep
|
||||
email={email}
|
||||
password={password}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <div />
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
await attemptLogin(email, password, setErrorLogin, router, false, true).then(() => {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
if (isAlreadyLoggedIn) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -84,80 +84,7 @@ export default function Login() {
|
||||
<Image src="/images/biglogo.png" height={90} width={120} alt="long logo" />
|
||||
</div>
|
||||
</Link>
|
||||
<form onChange={() => setErrorLogin(false)} onSubmit={(e) => e.preventDefault()}>
|
||||
<div className="bg-bunker w-full max-w-md mx-auto h-7/12 py-4 pt-8 px-6 rounded-xl drop-shadow-xl">
|
||||
<p className="text-3xl w-max mx-auto flex justify-center font-semibold text-bunker-100 mb-6">
|
||||
{t('login:login')}
|
||||
</p>
|
||||
<div className="flex items-center justify-center w-full md:p-2 rounded-lg mt-4 md:mt-0 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:email')}
|
||||
onChangeHandler={setEmail}
|
||||
type="email"
|
||||
value={email}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex items-center justify-center w-full md:p-2 rounded-lg md:mt-2 mt-6 max-h-24 md:max-h-28">
|
||||
<InputField
|
||||
label={t('common:password')}
|
||||
onChangeHandler={setPassword}
|
||||
type="password"
|
||||
value={password}
|
||||
placeholder=""
|
||||
isRequired
|
||||
autoComplete="current-password"
|
||||
id="current-password"
|
||||
/>
|
||||
<div className="absolute top-2 right-3 text-primary-700 hover:text-primary duration-200 cursor-pointer text-sm">
|
||||
<Link href="/verify-email">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:forgot-password')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!isLoading && errorLogin && <Error text={t('login:error-login') ?? ''} />}
|
||||
<div className="flex flex-col items-center justify-center w-full md:p-2 max-h-20 max-w-md mt-4 mx-auto text-sm">
|
||||
<div className="text-l mt-6 m-8 px-8 py-3 text-lg">
|
||||
<Button
|
||||
type="submit"
|
||||
text={t('login:login') ?? ''}
|
||||
onButtonPressed={loginCheck}
|
||||
loading={isLoading}
|
||||
size="lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="flex items-center justify-center w-full md:p-2 rounded-lg max-h-24 md:max-h-28">
|
||||
<p className="text-gray-400">I may have <Link href="/login"><u className="text-sky-500 cursor-pointer">forgotten my password.</u></Link></p>
|
||||
</div> */}
|
||||
</div>
|
||||
{false && (
|
||||
<div className="w-full p-2 flex flex-row items-center bg-white/10 text-gray-300 rounded-md max-w-md mx-auto mt-4">
|
||||
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-6 text-6xl" />
|
||||
{t('common:maintenance-alert')}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row items-center justify-center md:pb-4 mt-4">
|
||||
<p className="text-sm flex justify-center text-gray-400 w-max">
|
||||
{t('login:need-account')}
|
||||
</p>
|
||||
<Link href="/signup">
|
||||
<button
|
||||
type="button"
|
||||
className="text-primary-700 hover:text-primary duration-200 font-normal text-sm underline-offset-4 ml-1.5"
|
||||
>
|
||||
{t('login:create-account')}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
{renderStep(step)}
|
||||
<div className="absolute right-4 top-0 mt-4 flex items-center justify-center">
|
||||
<div className="w-48 mx-auto">
|
||||
<ListBox
|
||||
@@ -173,4 +100,4 @@ export default function Login() {
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'login']);
|
||||
export const getStaticProps = getTranslatedStaticProps(['auth', 'login', 'mfa']);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -12,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
|
||||
import { deriveArgonKey } from '../components/utilities/cryptography/crypto';
|
||||
import EmailVerifyOnPasswordReset from './api/auth/EmailVerifyOnPasswordReset';
|
||||
import getBackupEncryptedPrivateKey from './api/auth/getBackupEncryptedPrivateKey';
|
||||
import resetPasswordOnAccountRecovery from './api/auth/resetPasswordOnAccountRecovery';
|
||||
@@ -39,6 +42,7 @@ export default function PasswordReset() {
|
||||
const getEncryptedKeyHandler = async () => {
|
||||
try {
|
||||
const result = await getBackupEncryptedPrivateKey({ verificationToken });
|
||||
|
||||
setPrivateKey(
|
||||
Aes256Gcm.decrypt({
|
||||
ciphertext: result.encryptedPrivateKey,
|
||||
@@ -64,14 +68,6 @@ export default function PasswordReset() {
|
||||
});
|
||||
|
||||
if (!errorCheck) {
|
||||
// Generate a random pair of a public and a private key
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: newPassword
|
||||
.slice(0, 32)
|
||||
.padStart(32 + (newPassword.slice(0, 32).length - new Blob([newPassword]).size), '0')
|
||||
}) as { ciphertext: string; iv: string; tag: string };
|
||||
|
||||
client.init(
|
||||
{
|
||||
username: email,
|
||||
@@ -79,13 +75,51 @@ export default function PasswordReset() {
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
|
||||
const response = await resetPasswordOnAccountRecovery({
|
||||
verificationToken,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password: newPassword,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString('hex'),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
const response = await resetPasswordOnAccountRecovery({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
verificationToken
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function RequestNewInvite() {
|
||||
doesn't work, please reach out to us at support@infisical.com
|
||||
</p>
|
||||
<div className="">
|
||||
<Image src="/images/invitation-expired.svg" height={500} width={800} alt="google logo" />
|
||||
<Image src="/images/invitation-expired.svg" height={500} width={800} alt="invitation expired illustration" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function SettingsBilling() {
|
||||
{
|
||||
key: 2,
|
||||
name: 'Team',
|
||||
price: '$7',
|
||||
price: '$8',
|
||||
priceExplanation: t('billing:professional.price-explanation')!,
|
||||
text: 'For teams that want to improve their efficiency and security.',
|
||||
buttonTextMain: t('billing:upgrade')!,
|
||||
@@ -41,7 +41,7 @@ export default function SettingsBilling() {
|
||||
{
|
||||
key: 3,
|
||||
name: t('billing:professional.name')!,
|
||||
price: '$14',
|
||||
price: '$18',
|
||||
priceExplanation: t('billing:professional.price-explanation')!,
|
||||
text: t('billing:enterprise.text')!,
|
||||
subtext: t('billing:professional.subtext')!,
|
||||
|
||||
@@ -1,375 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { faCheck, faMagnifyingGlass, faPlus, faX } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { plans } from 'public/data/frequentConstants';
|
||||
|
||||
import Button from '@app/components/basic/buttons/Button';
|
||||
import AddIncidentContactDialog from '@app/components/basic/dialog/AddIncidentContactDialog';
|
||||
import AddUserDialog from '@app/components/basic/dialog/AddUserDialog';
|
||||
import UpgradePlanModal from '@app/components/basic/dialog/UpgradePlan';
|
||||
import InputField from '@app/components/basic/InputField';
|
||||
import UserTable from '@app/components/basic/table/UserTable';
|
||||
import NavHeader from '@app/components/navigation/NavHeader';
|
||||
import guidGenerator from '@app/components/utilities/randomId';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
|
||||
import addUserToOrg from '../../api/organization/addUserToOrg';
|
||||
import deleteIncidentContact from '../../api/organization/deleteIncidentContact';
|
||||
import getIncidentContacts from '../../api/organization/getIncidentContacts';
|
||||
import getOrganization from '../../api/organization/GetOrg';
|
||||
import getOrganizationSubscriptions from '../../api/organization/GetOrgSubscription';
|
||||
import getOrganizationUsers from '../../api/organization/GetOrgUsers';
|
||||
import renameOrg from '../../api/organization/renameOrg';
|
||||
import getUser from '../../api/user/getUser';
|
||||
import deleteWorkspace from '../../api/workspace/deleteWorkspace';
|
||||
import getWorkspaces from '../../api/workspace/getWorkspaces';
|
||||
import { OrgSettingsPage } from '@app/views/Settings/OrgSettingsPage';
|
||||
|
||||
export default function SettingsOrg() {
|
||||
const [buttonReady, setButtonReady] = useState(false);
|
||||
const router = useRouter();
|
||||
const host = window.location.origin;
|
||||
const [orgName, setOrgName] = useState('');
|
||||
const [emailUser, setEmailUser] = useState('');
|
||||
const [workspaceToBeDeletedName, setWorkspaceToBeDeletedName] = useState('');
|
||||
const [searchUsers, setSearchUsers] = useState('');
|
||||
const [isAddIncidentContactOpen, setIsAddIncidentContactOpen] = useState(false);
|
||||
const [isAddUserOpen, setIsAddUserOpen] = useState(router.asPath.split('?')[1] === 'invite');
|
||||
const [incidentContacts, setIncidentContacts] = useState<string[]>([]);
|
||||
const [searchIncidentContact, setSearchIncidentContact] = useState('');
|
||||
const [userList, setUserList] = useState<any[]>([]);
|
||||
const [personalEmail, setPersonalEmail] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [currentPlan, setCurrentPlan] = useState('');
|
||||
|
||||
const workspaceId = router.query.id as string;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const orgId = localStorage.getItem('orgData.id') as string;
|
||||
const org = await getOrganization({
|
||||
orgId
|
||||
});
|
||||
|
||||
setOrgName(org.name);
|
||||
const incidentContactsData = await getIncidentContacts(
|
||||
localStorage.getItem('orgData.id') as string
|
||||
);
|
||||
|
||||
setIncidentContacts(incidentContactsData?.map((contact) => contact.email));
|
||||
|
||||
const user = await getUser();
|
||||
setPersonalEmail(user.email);
|
||||
|
||||
const orgUsers = await getOrganizationUsers({
|
||||
orgId
|
||||
});
|
||||
|
||||
setUserList(
|
||||
orgUsers.map((orgUser) => ({
|
||||
key: guidGenerator(),
|
||||
firstName: orgUser.user?.firstName,
|
||||
lastName: orgUser.user?.lastName,
|
||||
email: orgUser.user?.email == null ? orgUser.inviteEmail : orgUser.user?.email,
|
||||
role: orgUser?.role,
|
||||
status: orgUser?.status,
|
||||
userId: orgUser.user?._id,
|
||||
membershipId: orgUser._id,
|
||||
publicKey: orgUser.user?.publicKey
|
||||
}))
|
||||
);
|
||||
|
||||
const subscriptions = await getOrganizationSubscriptions({
|
||||
orgId
|
||||
});
|
||||
if (subscriptions) {
|
||||
setCurrentPlan(subscriptions.data[0].plan.product);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const modifyOrgName = (newName: string) => {
|
||||
setButtonReady(true);
|
||||
setOrgName(newName);
|
||||
};
|
||||
|
||||
const submitChanges = (newOrgName: string) => {
|
||||
renameOrg(localStorage.getItem('orgData.id') as string, newOrgName);
|
||||
setButtonReady(false);
|
||||
};
|
||||
|
||||
const closeAddUserModal = () => {
|
||||
setIsAddUserOpen(false);
|
||||
};
|
||||
|
||||
const closeAddIncidentContactModal = () => {
|
||||
setIsAddIncidentContactOpen(false);
|
||||
};
|
||||
|
||||
const openAddUserModal = () => {
|
||||
setIsAddUserOpen(true);
|
||||
};
|
||||
|
||||
const openAddIncidentContactModal = () => {
|
||||
setIsAddIncidentContactOpen(true);
|
||||
};
|
||||
|
||||
const submitAddUserModal = async (newUserEmail: string) => {
|
||||
await addUserToOrg(newUserEmail, localStorage.getItem('orgData.id') as string);
|
||||
setEmail('');
|
||||
setIsAddUserOpen(false);
|
||||
router.reload();
|
||||
};
|
||||
|
||||
const deleteIncidentContactFully = (incidentContact: string) => {
|
||||
setIncidentContacts(incidentContacts.filter((contact) => contact !== incidentContact));
|
||||
deleteIncidentContact(localStorage.getItem('orgData.id') as string, incidentContact);
|
||||
};
|
||||
|
||||
/**
|
||||
* This function deleted a workspace.
|
||||
* It first checks if there is more than one workspace aviable. Otherwise, it doesn't delete
|
||||
* It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete.
|
||||
* It then deletes the workspace and forwards the user to another aviable workspace.
|
||||
*/
|
||||
const executeDeletingWorkspace = async () => {
|
||||
const userWorkspaces = await getWorkspaces();
|
||||
|
||||
if (userWorkspaces.length > 1) {
|
||||
if (
|
||||
userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name ===
|
||||
workspaceToBeDeletedName
|
||||
) {
|
||||
await deleteWorkspace(workspaceId);
|
||||
const ws = await getWorkspaces();
|
||||
router.push(`/dashboard/${ws[0]._id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||
<>
|
||||
<Head>
|
||||
<title>{t('common:head-title', { title: t('settings-org:title') })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full max-h-screen pb-2">
|
||||
<NavHeader pageName={t('settings-org:title')} />
|
||||
<AddIncidentContactDialog
|
||||
isOpen={isAddIncidentContactOpen}
|
||||
closeModal={closeAddIncidentContactModal}
|
||||
incidentContacts={incidentContacts}
|
||||
setIncidentContacts={setIncidentContacts}
|
||||
/>
|
||||
<div className="flex flex-row justify-between items-center ml-6 my-8 text-xl max-w-5xl">
|
||||
<div className="flex flex-col justify-start items-start text-3xl">
|
||||
<p className="font-semibold mr-4 text-gray-200">{t('settings-org:title')}</p>
|
||||
<p className="font-normal mr-4 text-gray-400 text-base">
|
||||
{t('settings-org:description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col ml-6 text-mineshaft-50 mr-6 max-w-8xl">
|
||||
<div className="flex flex-col">
|
||||
<div className="min-w-md mt-2 flex flex-col items-end pb-4">
|
||||
<div className="bg-white/5 rounded-md px-6 py-4 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">
|
||||
{t('common:display-name')}
|
||||
</p>
|
||||
<InputField
|
||||
label=""
|
||||
// label="Organization Name"
|
||||
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">
|
||||
<Button
|
||||
text={t('common:save-changes') as string}
|
||||
onButtonPressed={() => submitChanges(orgName)}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
active={buttonReady}
|
||||
iconDisabled={faCheck}
|
||||
textDisabled={t('common:saved') as string}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md px-6 pt-6 pb-2 flex flex-col items-start w-full mb-6">
|
||||
<p className="font-semibold mr-4 text-white text-xl">
|
||||
{t('section-members:org-members')}
|
||||
</p>
|
||||
<p className="mr-4 text-gray-400 mt-2 mb-2">
|
||||
{t('section-members:org-members-description')}
|
||||
</p>
|
||||
<AddUserDialog
|
||||
isOpen={isAddUserOpen && (userList.length < 5 || currentPlan !== plans.starter || host !== 'https://app.infisical.com')}
|
||||
closeModal={closeAddUserModal}
|
||||
submitModal={submitAddUserModal}
|
||||
email={emailUser}
|
||||
setEmail={setEmailUser}
|
||||
currentPlan={currentPlan}
|
||||
orgName={orgName}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={isAddUserOpen && userList.length >= 5 && currentPlan === plans.starter && host === 'https://app.infisical.com'}
|
||||
onClose={closeAddUserModal}
|
||||
text="You can add more members if you switch to Infisical's Team plan."
|
||||
/>
|
||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||
<div className="pb-1 w-full flex flex-row items-start">
|
||||
<div className="h-10 w-full bg-white/5 mt-2 flex items-center rounded-md flex-row ">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-l-md py-3 pl-4 pr-2 text-gray-400"
|
||||
icon={faMagnifyingGlass}
|
||||
/>
|
||||
<input
|
||||
className="pl-2 text-gray-400 rounded-r-md bg-white/5 w-full h-full outline-none"
|
||||
value={searchUsers}
|
||||
onChange={(e) => setSearchUsers(e.target.value)}
|
||||
placeholder={t('section-members:search-members') as string}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 ml-2 min-w-max flex flex-row items-start justify-start">
|
||||
<Button
|
||||
text={t('section-members:add-member') as string}
|
||||
onButtonPressed={openAddUserModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{userList && (
|
||||
<div className="overflow-y-auto w-full">
|
||||
<UserTable
|
||||
userData={userList}
|
||||
changeData={setUserList}
|
||||
myUser={personalEmail}
|
||||
filter={searchUsers.toLowerCase()}
|
||||
resendInvite={submitAddUserModal}
|
||||
isOrg
|
||||
// onClick={openDeleteModal}
|
||||
// deleteUser={deleteMembership}
|
||||
// setUserIdToBeDeleted={setUserIdToBeDeleted}
|
||||
/>
|
||||
</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">
|
||||
<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('section-incident:incident-contacts')}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mb-2 min-w-max">
|
||||
{t('section-incident:incident-contacts-description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 mb-2 min-w-max flex flex-row items-end justify-center">
|
||||
<Button
|
||||
text={t('section-incident:add-contact') as string}
|
||||
onButtonPressed={openAddIncidentContactModal}
|
||||
color="mineshaft"
|
||||
size="md"
|
||||
icon={faPlus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-12 w-full max-w-5xl bg-white/5 mt-2 flex items-center rounded-t-md flwex-row">
|
||||
<FontAwesomeIcon
|
||||
className="bg-white/5 rounded-tl-md py-4 pl-4 pr-2 text-gray-400"
|
||||
icon={faMagnifyingGlass}
|
||||
/>
|
||||
<input
|
||||
className="pl-2 text-gray-400 rounded-tr-md bg-white/5 w-full h-full outline-none"
|
||||
value={searchIncidentContact}
|
||||
onChange={(e) => setSearchIncidentContact(e.target.value)}
|
||||
placeholder={t('common:search') as string}
|
||||
/>
|
||||
</div>
|
||||
{incidentContacts?.filter((incidentEmail) =>
|
||||
incidentEmail.includes(searchIncidentContact)
|
||||
).length > 0 ? (
|
||||
incidentContacts
|
||||
.filter((incidentEmail) => incidentEmail.includes(searchIncidentContact))
|
||||
.map((contact) => (
|
||||
<div
|
||||
key={guidGenerator()}
|
||||
className="flex flex-row items-center justify-between max-w-5xl px-4 py-3 hover:bg-white/5 border-t border-gray-600 w-full"
|
||||
>
|
||||
<p className="text-gray-300">{contact}</p>
|
||||
<div className="opacity-50 hover:opacity-100 duration-200">
|
||||
<Button
|
||||
onButtonPressed={() => deleteIncidentContactFully(contact)}
|
||||
color="red"
|
||||
size="icon-sm"
|
||||
icon={faX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="w-full flex flex-row justify-center mt-6 max-w-4xl ml-6">
|
||||
<p className="text-gray-400">{t('section-incident:no-incident-contacts')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* <div className="border-l border-red pb-4 pl-6 flex flex-col items-start flex flex-col items-start w-full mb-6 mt-4 pt-2 max-w-6xl">
|
||||
<p className="text-xl font-bold text-red">
|
||||
Danger Zone
|
||||
</p>
|
||||
<p className="mt-4 text-md text-gray-400">
|
||||
As soon as you delete an organization, you will
|
||||
not be able to undo it. This will immediately
|
||||
remove all organization members and cancel your
|
||||
subscription. If you still want to do that,
|
||||
please enter the name of the organization below.
|
||||
</p>
|
||||
<div className="max-h-28 w-full max-w-xl mr-auto mt-8 max-w-xl">
|
||||
<InputField
|
||||
label="Organization to be Deleted"
|
||||
onChangeHandler={
|
||||
setWorkspaceToBeDeletedName
|
||||
}
|
||||
type="varName"
|
||||
value={workspaceToBeDeletedName}
|
||||
placeholder=""
|
||||
isRequired
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-6 w-full max-w-xl inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-400 hover:bg-red hover:text-white hover:font-bold hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={executeDeletingWorkspace}
|
||||
>
|
||||
Delete Project
|
||||
</button>
|
||||
<p className="mt-0.5 ml-1 text-xs text-gray-500">
|
||||
Note: You can only delete a project in case you
|
||||
have more than one.
|
||||
</p>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<OrgSettingsPage />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
||||
import changePassword from '@app/components/utilities/cryptography/changePassword';
|
||||
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
||||
import { getTranslatedServerSideProps } from '@app/components/utilities/withTranslateProps';
|
||||
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
|
||||
|
||||
import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog';
|
||||
import getAPIKeys from '../../api/apiKey/getAPIKeys';
|
||||
@@ -100,6 +101,7 @@ export default function PersonalSettings() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SecuritySection />
|
||||
<div className="bg-white/5 rounded-md px-6 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">
|
||||
|
||||
@@ -11,6 +11,7 @@ import DownloadBackupPDF from '@app/components/signup/DonwloadBackupPDFStep';
|
||||
import EnterEmailStep from '@app/components/signup/EnterEmailStep';
|
||||
import TeamInviteStep from '@app/components/signup/TeamInviteStep';
|
||||
import UserInfoStep from '@app/components/signup/UserInfoStep';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
import { getTranslatedStaticProps } from '@app/components/utilities/withTranslateProps';
|
||||
|
||||
import checkEmailVerificationCode from './api/auth/CheckEmailVerificationCode';
|
||||
@@ -28,7 +29,6 @@ export default function SignUp() {
|
||||
const [codeError, setCodeError] = useState(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const router = useRouter();
|
||||
const [verificationToken, setVerificationToken] = useState('');
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -59,7 +59,8 @@ export default function SignUp() {
|
||||
// Checking if the code matches the email.
|
||||
const response = await checkEmailVerificationCode({ email, code });
|
||||
if (response.status === 200) {
|
||||
setVerificationToken((await response.json()).token);
|
||||
const {token} = await response.json();
|
||||
SecurityClient.setSignupToken(token);
|
||||
setStep(3);
|
||||
} else {
|
||||
setCodeError(true);
|
||||
@@ -94,7 +95,6 @@ export default function SignUp() {
|
||||
/>
|
||||
) : step === 3 ? (
|
||||
<UserInfoStep
|
||||
verificationToken={verificationToken}
|
||||
incrementStep={incrementStep}
|
||||
email={email}
|
||||
password={password}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
@@ -17,7 +19,12 @@ import InputField from '@app/components/basic/InputField';
|
||||
import attemptLogin from '@app/components/utilities/attemptLogin';
|
||||
import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
|
||||
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
|
||||
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
|
||||
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
|
||||
import { saveTokenToLocalStorage } from '@app/components/utilities/saveTokenToLocalStorage';
|
||||
import SecurityClient from '@app/components/utilities/SecurityClient';
|
||||
import getOrganizations from '@app/pages/api/organization/getOrgs';
|
||||
import getOrganizationUserProjects from '@app/pages/api/organization/GetOrgUserProjects';
|
||||
|
||||
import completeAccountInformationSignupInvite from './api/auth/CompleteAccountInformationSignupInvite';
|
||||
import verifySignupInvite from './api/auth/VerifySignupInvite';
|
||||
@@ -75,17 +82,10 @@ export default function SignupInvite() {
|
||||
const pair = nacl.box.keyPair();
|
||||
const secretKeyUint8Array = pair.secretKey;
|
||||
const publicKeyUint8Array = pair.publicKey;
|
||||
const PRIVATE_KEY = encodeBase64(secretKeyUint8Array);
|
||||
const PUBLIC_KEY = encodeBase64(publicKeyUint8Array);
|
||||
const privateKey = encodeBase64(secretKeyUint8Array);
|
||||
const publicKey = encodeBase64(publicKeyUint8Array);
|
||||
|
||||
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
|
||||
text: PRIVATE_KEY,
|
||||
secret: password
|
||||
.slice(0, 32)
|
||||
.padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), '0')
|
||||
});
|
||||
|
||||
localStorage.setItem('PRIVATE_KEY', PRIVATE_KEY);
|
||||
localStorage.setItem('PRIVATE_KEY', privateKey);
|
||||
|
||||
client.init(
|
||||
{
|
||||
@@ -94,35 +94,83 @@ export default function SignupInvite() {
|
||||
},
|
||||
async () => {
|
||||
client.createVerifier(async (err, result) => {
|
||||
let response = await completeAccountInformationSignupInvite({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
publicKey: PUBLIC_KEY,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier,
|
||||
token: verificationToken
|
||||
});
|
||||
try {
|
||||
const derivedKey = await deriveArgonKey({
|
||||
password,
|
||||
salt: result.salt,
|
||||
mem: 65536,
|
||||
time: 3,
|
||||
parallelism: 1,
|
||||
hashLen: 32
|
||||
});
|
||||
|
||||
// if everything works, go the main dashboard page.
|
||||
if (!errorCheck && response.status === 200) {
|
||||
response = await response.json();
|
||||
if (!derivedKey) throw new Error('Failed to derive key from password');
|
||||
|
||||
localStorage.setItem('publicKey', PUBLIC_KEY);
|
||||
localStorage.setItem('encryptedPrivateKey', ciphertext);
|
||||
localStorage.setItem('iv', iv);
|
||||
localStorage.setItem('tag', tag);
|
||||
const key = crypto.randomBytes(32);
|
||||
|
||||
// create encrypted private key by encrypting the private
|
||||
// key with the symmetric key [key]
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: privateKey,
|
||||
secret: key
|
||||
});
|
||||
|
||||
// create the protected key by encrypting the symmetric key
|
||||
// [key] with the derived key
|
||||
const {
|
||||
ciphertext: protectedKey,
|
||||
iv: protectedKeyIV,
|
||||
tag: protectedKeyTag
|
||||
} = Aes256Gcm.encrypt({
|
||||
text: key.toString('hex'),
|
||||
secret: Buffer.from(derivedKey.hash)
|
||||
});
|
||||
|
||||
const {
|
||||
token: jwtToken
|
||||
} = await completeAccountInformationSignupInvite({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
encryptedPrivateKeyIV,
|
||||
encryptedPrivateKeyTag,
|
||||
salt: result.salt,
|
||||
verifier: result.verifier
|
||||
});
|
||||
|
||||
// unset temporary signup JWT token and set JWT token
|
||||
SecurityClient.setSignupToken('');
|
||||
SecurityClient.setToken(jwtToken);
|
||||
|
||||
try {
|
||||
await attemptLogin(email, password, setErrorLogin, router, false, false);
|
||||
setStep(3);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.log('Error', error);
|
||||
}
|
||||
saveTokenToLocalStorage({
|
||||
protectedKey,
|
||||
protectedKeyIV,
|
||||
protectedKeyTag,
|
||||
publicKey,
|
||||
encryptedPrivateKey,
|
||||
iv: encryptedPrivateKeyIV,
|
||||
tag: encryptedPrivateKeyTag,
|
||||
privateKey
|
||||
});
|
||||
|
||||
const userOrgs = await getOrganizations();
|
||||
|
||||
const orgId = userOrgs[0]._id;
|
||||
localStorage.setItem('orgData.id', orgId);
|
||||
|
||||
setStep(3);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -152,7 +200,7 @@ export default function SignupInvite() {
|
||||
// user will have temp token if doesn't have an account
|
||||
// then continue with account setup workflow
|
||||
if (res?.token) {
|
||||
setVerificationToken(res.token);
|
||||
SecurityClient.setSignupToken(res.token);
|
||||
setStep(2);
|
||||
} else {
|
||||
// user will be redirected to dashboard
|
||||
|
||||
Reference in New Issue
Block a user