@@ -64,7 +78,7 @@ const ActivityLogsRow = ({
)
.join(' and ')}
- {row.user}
+ {renderUser()}
{row.channel}
{timeSince(new Date(row.createdAt))}
diff --git a/frontend/src/ee/components/SecretVersionList.tsx b/frontend/src/ee/components/SecretVersionList.tsx
index 3f16a228d..dbf7c5bf2 100644
--- a/frontend/src/ee/components/SecretVersionList.tsx
+++ b/frontend/src/ee/components/SecretVersionList.tsx
@@ -76,7 +76,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
}, [secretId]);
return (
-
+
{t('dashboard:sidebar.version-history')}
{isLoading ? (
@@ -102,7 +102,7 @@ const SecretVersionList = ({ secretId }: { secretId: string }) => {
-
+
{new Date(version.createdAt).toLocaleDateString('en-US', {
year: 'numeric',
month: '2-digit',
diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx
index c2035bbba..89d947068 100644
--- a/frontend/src/hooks/api/index.tsx
+++ b/frontend/src/hooks/api/index.tsx
@@ -2,6 +2,9 @@ export * from './auth';
export * from './incidentContacts';
export * from './keys';
export * from './organization';
+export * from './secrets';
+export * from './secretSnapshots';
+export * from './serviceAccounts';
export * from './serviceTokens';
export * from './subscriptions';
export * from './tags';
diff --git a/frontend/src/hooks/api/integrationAuth/index.tsx b/frontend/src/hooks/api/integrationAuth/index.tsx
index c02938985..367534305 100644
--- a/frontend/src/hooks/api/integrationAuth/index.tsx
+++ b/frontend/src/hooks/api/integrationAuth/index.tsx
@@ -1,4 +1,7 @@
export {
useGetIntegrationAuthApps,
useGetIntegrationAuthById,
- useGetIntegrationAuthTeams} from './queries';
\ No newline at end of file
+ useGetIntegrationAuthRailwayEnvironments,
+ useGetIntegrationAuthRailwayServices,
+ useGetIntegrationAuthTeams,
+ useGetIntegrationAuthVercelBranches} from './queries';
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/queries.tsx b/frontend/src/hooks/api/integrationAuth/queries.tsx
index 56a6ee45e..02a8b6c51 100644
--- a/frontend/src/hooks/api/integrationAuth/queries.tsx
+++ b/frontend/src/hooks/api/integrationAuth/queries.tsx
@@ -4,13 +4,37 @@ import { apiRequest } from "@app/config/request";
import {
App,
+ Environment,
IntegrationAuth,
- Team} from './types';
+ Service,
+ Team
+} from './types';
const integrationAuthKeys = {
getIntegrationAuthById: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuth'] as const,
getIntegrationAuthApps: (integrationAuthId: string, teamId?: string) => [{ integrationAuthId, teamId }, 'integrationAuthApps'] as const,
- getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const
+ getIntegrationAuthTeams: (integrationAuthId: string) => [{ integrationAuthId }, 'integrationAuthTeams'] as const,
+ getIntegrationAuthVercelBranches: ({
+ integrationAuthId,
+ appId,
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, 'integrationAuthVercelBranches'] as const,
+ getIntegrationAuthRailwayEnvironments: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, 'integrationAuthRailwayEnvironments'] as const,
+ getIntegrationAuthRailwayServices: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, 'integrationAuthRailwayServices'] as const
}
const fetchIntegrationAuthById = async (integrationAuthId: string) => {
@@ -38,6 +62,54 @@ const fetchIntegrationAuthTeams = async (integrationAuthId: string) => {
return data.teams;
}
+const fetchIntegrationAuthVercelBranches = async ({
+ integrationAuthId,
+ appId
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ const { data: { branches } } = await apiRequest.get<{ branches: string[] }>(`/api/v1/integration-auth/${integrationAuthId}/vercel/branches`, {
+ params: {
+ appId
+ }
+ });
+
+ return branches;
+};
+
+const fetchIntegrationAuthRailwayEnvironments = async ({
+ integrationAuthId,
+ appId
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ const { data: { environments } } = await apiRequest.get<{ environments: Environment[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/environments`, {
+ params: {
+ appId
+ }
+ });
+
+ return environments;
+}
+
+const fetchIntegrationAuthRailwayServices = async ({
+ integrationAuthId,
+ appId
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ const { data: { services } } = await apiRequest.get<{ services: Service[] }>(`/api/v1/integration-auth/${integrationAuthId}/railway/services`, {
+ params: {
+ appId
+ }
+ });
+
+ return services;
+}
+
export const useGetIntegrationAuthById = (integrationAuthId: string) => {
return useQuery({
queryKey: integrationAuthKeys.getIntegrationAuthById(integrationAuthId),
@@ -46,7 +118,6 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
});
}
-// TODO: fix to teamId
export const useGetIntegrationAuthApps = ({
integrationAuthId,
teamId
@@ -70,4 +141,64 @@ export const useGetIntegrationAuthTeams = (integrationAuthId: string) => {
queryFn: () => fetchIntegrationAuthTeams(integrationAuthId),
enabled: true
});
+}
+
+export const useGetIntegrationAuthVercelBranches = ({
+ integrationAuthId,
+ appId,
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthVercelBranches({
+ integrationAuthId,
+ appId,
+ }),
+ queryFn: () => fetchIntegrationAuthVercelBranches({
+ integrationAuthId,
+ appId,
+ }),
+ enabled: true
+ });
+}
+
+export const useGetIntegrationAuthRailwayEnvironments = ({
+ integrationAuthId,
+ appId
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthRailwayEnvironments({
+ integrationAuthId,
+ appId,
+ }),
+ queryFn: () => fetchIntegrationAuthRailwayEnvironments({
+ integrationAuthId,
+ appId,
+ }),
+ enabled: true
+ });
+}
+
+export const useGetIntegrationAuthRailwayServices = ({
+ integrationAuthId,
+ appId
+}: {
+ integrationAuthId: string;
+ appId: string;
+}) => {
+ return useQuery({
+ queryKey: integrationAuthKeys.getIntegrationAuthRailwayServices({
+ integrationAuthId,
+ appId,
+ }),
+ queryFn: () => fetchIntegrationAuthRailwayServices({
+ integrationAuthId,
+ appId,
+ }),
+ enabled: true
+ });
}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/integrationAuth/types.ts b/frontend/src/hooks/api/integrationAuth/types.ts
index 4ba19c192..7ea8799a5 100644
--- a/frontend/src/hooks/api/integrationAuth/types.ts
+++ b/frontend/src/hooks/api/integrationAuth/types.ts
@@ -15,4 +15,14 @@ export type App = {
export type Team = {
name: string;
teamId: string;
+}
+
+export type Environment = {
+ name: string;
+ environmentId: string;
+}
+
+export type Service = {
+ name: string;
+ serviceId: string;
}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/secretSnapshots/index.tsx b/frontend/src/hooks/api/secretSnapshots/index.tsx
new file mode 100644
index 000000000..7d6b368e7
--- /dev/null
+++ b/frontend/src/hooks/api/secretSnapshots/index.tsx
@@ -0,0 +1,6 @@
+export {
+ useGetSnapshotSecrets,
+ useGetWorkspaceSecretSnapshots,
+ useGetWsSnapshotCount,
+ usePerformSecretRollback
+} from './queries';
diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx
new file mode 100644
index 000000000..3aa9dfbe5
--- /dev/null
+++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx
@@ -0,0 +1,154 @@
+/* eslint-disable no-param-reassign */
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import {
+ decryptAssymmetric,
+ decryptSymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { apiRequest } from '@app/config/request';
+
+import { DecryptedSecret } from '../secrets/types';
+import {
+ GetWorkspaceSecretSnapshotsDTO,
+ TSecretRollbackDTO,
+ TSnapshotSecret,
+ TSnapshotSecretProps,
+ TWorkspaceSecretSnapshot
+} from './types';
+
+export const secretSnapshotKeys = {
+ list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const,
+ snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
+ count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot']
+};
+
+const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => {
+ const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
+ `/api/v1/workspace/${workspaceId}/secret-snapshots`,
+ {
+ params: {
+ limit,
+ offset
+ }
+ }
+ );
+
+ return res.data.secretSnapshots;
+};
+
+export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
+ useInfiniteQuery({
+ enabled: Boolean(dto.workspaceId),
+ queryKey: secretSnapshotKeys.list(dto.workspaceId),
+ queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam),
+ getNextPageParam: (lastPage, pages) =>
+ lastPage.length !== 0 ? pages.length * dto.limit : undefined
+ });
+
+const fetchSnapshotEncSecrets = async (snapshotId: string) => {
+ const res = await apiRequest.get<{ secretSnapshot: TSnapshotSecret }>(
+ `/api/v1/secret-snapshot/${snapshotId}`
+ );
+ return res.data.secretSnapshot;
+};
+
+export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotSecretProps) =>
+ useQuery({
+ queryKey: secretSnapshotKeys.snapshotSecrets(snapshotId),
+ enabled: Boolean(snapshotId && decryptFileKey),
+ queryFn: () => fetchSnapshotEncSecrets(snapshotId),
+ select: (data) => {
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+ const latestKey = decryptFileKey;
+ const key = decryptAssymmetric({
+ ciphertext: latestKey.encryptedKey,
+ nonce: latestKey.nonce,
+ publicKey: latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const sharedSecrets: DecryptedSecret[] = [];
+ const personalSecrets: Record
= {};
+ data.secretVersions
+ .filter(({ environment }) => environment === env)
+ .forEach((encSecret) => {
+ const secretKey = decryptSymmetric({
+ ciphertext: encSecret.secretKeyCiphertext,
+ iv: encSecret.secretKeyIV,
+ tag: encSecret.secretKeyTag,
+ key
+ });
+
+ const secretValue = decryptSymmetric({
+ ciphertext: encSecret.secretValueCiphertext,
+ iv: encSecret.secretValueIV,
+ tag: encSecret.secretValueTag,
+ key
+ });
+
+ const secretComment = '';
+
+ const decryptedSecret = {
+ _id: encSecret.secret,
+ env: encSecret.environment,
+ key: secretKey,
+ value: secretValue,
+ tags: encSecret.tags,
+ comment: secretComment,
+ createdAt: encSecret.createdAt,
+ updatedAt: encSecret.updatedAt,
+ type: 'modified'
+ };
+
+ if (encSecret.type === 'personal') {
+ personalSecrets[decryptedSecret.key] = { id: encSecret.secret, value: secretValue };
+ } else {
+ sharedSecrets.push(decryptedSecret);
+ }
+ });
+
+ sharedSecrets.forEach((val) => {
+ if (personalSecrets?.[val.key]) {
+ val.idOverride = personalSecrets[val.key].id;
+ val.valueOverride = personalSecrets[val.key].value;
+ val.overrideAction = 'modified';
+ }
+ });
+
+ return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt };
+ }
+ });
+
+const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => {
+ const res = await apiRequest.get<{ count: number }>(
+ `/api/v1/workspace/${workspaceId}/secret-snapshots/count`
+ );
+ return res.data.count;
+};
+
+export const useGetWsSnapshotCount = (workspaceId: string) =>
+ useQuery({
+ enabled: Boolean(workspaceId),
+ queryKey: secretSnapshotKeys.count(workspaceId),
+ queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId)
+ });
+
+export const usePerformSecretRollback = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TSecretRollbackDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`,
+ {
+ version: dto.version
+ }
+ );
+ return data;
+ },
+ onSuccess: (_, dto) => {
+ queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
+ queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/secretSnapshots/types.ts b/frontend/src/hooks/api/secretSnapshots/types.ts
new file mode 100644
index 000000000..dbe8ccd17
--- /dev/null
+++ b/frontend/src/hooks/api/secretSnapshots/types.ts
@@ -0,0 +1,32 @@
+import { UserWsKeyPair } from '../keys/types';
+import { EncryptedSecretVersion } from '../secrets/types';
+
+export type TWorkspaceSecretSnapshot = {
+ _id: string;
+ workspace: string;
+ version: number;
+ secretVersions: string[];
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+};
+
+export type TSnapshotSecret = Omit & {
+ secretVersions: EncryptedSecretVersion[];
+};
+
+export type TSnapshotSecretProps = {
+ snapshotId: string;
+ env: string;
+ decryptFileKey: UserWsKeyPair;
+};
+
+export type GetWorkspaceSecretSnapshotsDTO = {
+ workspaceId: string;
+ limit: number;
+};
+
+export type TSecretRollbackDTO = {
+ workspaceId: string;
+ version: number;
+};
diff --git a/frontend/src/hooks/api/secrets/index.ts b/frontend/src/hooks/api/secrets/index.ts
new file mode 100644
index 000000000..c27cecfb1
--- /dev/null
+++ b/frontend/src/hooks/api/secrets/index.ts
@@ -0,0 +1 @@
+export { useBatchSecretsOp, useGetProjectSecrets, useGetSecretVersion } from './queries';
diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx
new file mode 100644
index 000000000..031c661d1
--- /dev/null
+++ b/frontend/src/hooks/api/secrets/queries.tsx
@@ -0,0 +1,197 @@
+/* eslint-disable no-param-reassign */
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import {
+ decryptAssymmetric,
+ decryptSymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { apiRequest } from '@app/config/request';
+
+import { secretSnapshotKeys } from '../secretSnapshots/queries';
+import {
+ BatchSecretDTO,
+ DecryptedSecret,
+ EncryptedSecret,
+ EncryptedSecretVersion,
+ GetProjectSecretsDTO,
+ GetSecretVersionsDTO
+} from './types';
+
+export const secretKeys = {
+ // this is also used in secretSnapshot part
+ getProjectSecret: (workspaceId: string, env: string | string[]) => [{ workspaceId, env }, 'secrets'],
+ getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
+};
+
+const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => {
+ if (typeof env === 'string') {
+ const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
+ params: {
+ environment: env,
+ workspaceId
+ }
+ });
+ return data.secrets;
+ }
+
+ if (typeof env === 'object') {
+ let allEnvData: any = [];
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const envPoint of env) {
+ // eslint-disable-next-line no-await-in-loop
+ const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
+ params: {
+ environment: envPoint,
+ workspaceId
+ }
+ });
+ allEnvData = allEnvData.concat(data.secrets);
+ }
+
+ return allEnvData;
+ // eslint-disable-next-line no-else-return
+ } else {
+ return null;
+ }
+
+};
+
+export const useGetProjectSecrets = ({
+ workspaceId,
+ env,
+ decryptFileKey,
+ isPaused
+}: GetProjectSecretsDTO) =>
+ useQuery({
+ // wait for all values to be available
+ enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
+ queryKey: secretKeys.getProjectSecret(workspaceId, env),
+ queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
+ select: (data) => {
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+ const latestKey = decryptFileKey;
+ const key = decryptAssymmetric({
+ ciphertext: latestKey.encryptedKey,
+ nonce: latestKey.nonce,
+ publicKey: latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const sharedSecrets: DecryptedSecret[] = [];
+ const personalSecrets: Record = {};
+ // this used for add-only mode in dashboard
+ // type won't be there thus only one key is shown
+ const duplicateSecretKey: Record = {};
+ data.forEach((encSecret: EncryptedSecret) => {
+ const secretKey = decryptSymmetric({
+ ciphertext: encSecret.secretKeyCiphertext,
+ iv: encSecret.secretKeyIV,
+ tag: encSecret.secretKeyTag,
+ key
+ });
+
+ const secretValue = decryptSymmetric({
+ ciphertext: encSecret.secretValueCiphertext,
+ iv: encSecret.secretValueIV,
+ tag: encSecret.secretValueTag,
+ key
+ });
+
+ const secretComment = decryptSymmetric({
+ ciphertext: encSecret.secretCommentCiphertext,
+ iv: encSecret.secretCommentIV,
+ tag: encSecret.secretCommentTag,
+ key
+ });
+
+ const decryptedSecret = {
+ _id: encSecret._id,
+ env: encSecret.environment,
+ key: secretKey,
+ value: secretValue,
+ tags: encSecret.tags,
+ comment: secretComment,
+ createdAt: encSecret.createdAt,
+ updatedAt: encSecret.updatedAt
+ };
+
+ if (encSecret.type === 'personal') {
+ personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { id: encSecret._id, value: secretValue };
+ } else {
+ if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
+ sharedSecrets.push(decryptedSecret);
+ }
+ duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
+ }
+ });
+ sharedSecrets.forEach((val) => {
+ if (personalSecrets?.[val.key]) {
+ val.idOverride = personalSecrets[val.key].id;
+ val.valueOverride = personalSecrets[val.key].value;
+ val.overrideAction = 'modified';
+ }
+ });
+
+ return { secrets: sharedSecrets };
+ }
+ });
+
+const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
+ const { data } = await apiRequest.get<{ secretVersions: EncryptedSecretVersion[] }>(
+ `/api/v1/secret/${secretId}/secret-versions`,
+ {
+ params: {
+ limit,
+ offset
+ }
+ }
+ );
+ return data.secretVersions;
+};
+
+export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
+ useQuery({
+ enabled: Boolean(dto.secretId && dto.decryptFileKey),
+ queryKey: secretKeys.getSecretVersion(dto.secretId),
+ queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
+ select: (data) => {
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+ const latestKey = dto.decryptFileKey;
+ const key = decryptAssymmetric({
+ ciphertext: latestKey.encryptedKey,
+ nonce: latestKey.nonce,
+ publicKey: latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ return data
+ .map((el) => ({
+ createdAt: el.createdAt,
+ id: el._id,
+ value: decryptSymmetric({
+ ciphertext: el.secretValueCiphertext,
+ iv: el.secretValueIV,
+ tag: el.secretValueTag,
+ key
+ })
+ }))
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
+ }
+ });
+
+export const useBatchSecretsOp = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, BatchSecretDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post('/api/v2/secrets/batch', dto);
+ return data;
+ },
+ onSuccess: (_, dto) => {
+ queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment));
+ queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
+ queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
+ }
+ });
+};
diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts
new file mode 100644
index 000000000..567fecc9a
--- /dev/null
+++ b/frontend/src/hooks/api/secrets/types.ts
@@ -0,0 +1,104 @@
+import type { UserWsKeyPair } from '../keys/types';
+import type { WsTag } from '../tags/types';
+
+export type EncryptedSecret = {
+ _id: string;
+ version: number;
+ workspace: string;
+ type: 'shared' | 'personal';
+ environment: string;
+ secretKeyCiphertext: string;
+ secretKeyIV: string;
+ secretKeyTag: string;
+ secretValueCiphertext: string;
+ secretValueIV: string;
+ secretValueTag: string;
+ __v: number;
+ createdAt: string;
+ updatedAt: string;
+ secretCommentCiphertext: string;
+ secretCommentIV: string;
+ secretCommentTag: string;
+ tags: WsTag[];
+};
+
+export type DecryptedSecret = {
+ _id: string;
+ key: string;
+ value: string;
+ comment: string;
+ tags: WsTag[];
+ createdAt: string;
+ updatedAt: string;
+ env: string;
+ valueOverride?: string;
+ idOverride?: string;
+ overrideAction?: string;
+};
+
+export type EncryptedSecretVersion = {
+ _id: string;
+ secret: string;
+ version: number;
+ workspace: string;
+ type: string;
+ environment: string;
+ isDeleted: boolean;
+ secretKeyCiphertext: string;
+ secretKeyIV: string;
+ secretKeyTag: string;
+ secretValueCiphertext: string;
+ secretValueIV: string;
+ secretValueTag: string;
+ tags: WsTag[];
+ __v: number;
+ createdAt: string;
+ updatedAt: string;
+};
+
+// dto
+type SecretTagArg = { _id: string; name: string; slug: string };
+
+export type UpdateSecretArg = {
+ _id: string;
+ type: 'shared' | 'personal';
+ secretKeyCiphertext: string;
+ secretKeyIV: string;
+ secretKeyTag: string;
+ secretValueCiphertext: string;
+ secretValueIV: string;
+ secretValueTag: string;
+ secretCommentCiphertext: string;
+ secretCommentIV: string;
+ secretCommentTag: string;
+ tags: SecretTagArg[];
+};
+
+export type CreateSecretArg = Omit;
+
+export type DeleteSecretArg = { _id: string };
+
+export type BatchSecretDTO = {
+ workspaceId: string;
+ environment: string;
+ requests: Array<
+ | { method: 'POST'; secret: CreateSecretArg }
+ | { method: 'PATCH'; secret: UpdateSecretArg }
+ | { method: 'DELETE'; secret: DeleteSecretArg }
+ >;
+};
+
+export type GetProjectSecretsDTO = {
+ workspaceId: string;
+ env: string | string[];
+ decryptFileKey: UserWsKeyPair;
+ isPaused?: boolean;
+ onSuccess?: (data: DecryptedSecret[]) => void;
+};
+
+export type GetSecretVersionsDTO = {
+ secretId: string;
+ limit: number;
+ offset: number;
+ decryptFileKey: UserWsKeyPair;
+};
diff --git a/frontend/src/hooks/api/serviceAccounts/index.tsx b/frontend/src/hooks/api/serviceAccounts/index.tsx
new file mode 100644
index 000000000..a063d40dd
--- /dev/null
+++ b/frontend/src/hooks/api/serviceAccounts/index.tsx
@@ -0,0 +1,10 @@
+export {
+ useCreateServiceAccount,
+ useCreateServiceAccountProjectLevelPermission,
+ useDeleteServiceAccount,
+ useDeleteServiceAccountProjectLevelPermission,
+ useGetServiceAccountById,
+ useGetServiceAccountProjectLevelPermissions,
+ useGetServiceAccounts,
+ useRenameServiceAccount
+} from './queries';
\ No newline at end of file
diff --git a/frontend/src/hooks/api/serviceAccounts/queries.tsx b/frontend/src/hooks/api/serviceAccounts/queries.tsx
new file mode 100644
index 000000000..ec7888547
--- /dev/null
+++ b/frontend/src/hooks/api/serviceAccounts/queries.tsx
@@ -0,0 +1,137 @@
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+
+import { apiRequest } from '@app/config/request';
+
+import {
+ CreateServiceAccountDTO,
+ CreateServiceAccountRes,
+ CreateServiceAccountWorkspacePermissionDTO,
+ DeleteServiceAccountWorkspacePermissionDTO,
+ RenameServiceAccountDTO,
+ ServiceAccount,
+ ServiceAccountWorkspacePermission
+} from './types';
+
+const serviceAccountKeys = {
+ getServiceAccountById: (serviceAccountId: string) => [{ serviceAccountId }, 'service-account'] as const,
+ getServiceAccounts: (organizationID: string) => [{ organizationID }, 'service-accounts'] as const,
+ getServiceAccountProjectLevelPermissions: (serviceAccountId: string) => [{ serviceAccountId }, 'service-account-project-level-permissions'] as const
+}
+
+const fetchServiceAccounts = async (organizationID: string) => {
+ const { data } = await apiRequest.get<{ serviceAccounts: ServiceAccount[] }>(
+ `/api/v2/organizations/${organizationID}/service-accounts`
+ );
+
+ return data.serviceAccounts;
+}
+
+const fetchServiceAccountById = async (serviceAccountId: string) => {
+ const { data } = await apiRequest.get<{ serviceAccount: ServiceAccount }>(
+ `/api/v2/service-accounts/${serviceAccountId}`
+ );
+
+ return data.serviceAccount;
+}
+
+export const useGetServiceAccounts = (organizationID: string) =>
+ useQuery({
+ queryKey: serviceAccountKeys.getServiceAccounts(organizationID),
+ queryFn: () => fetchServiceAccounts(organizationID),
+ enabled: Boolean(organizationID)
+ });
+
+export const useGetServiceAccountById = (serviceAccountId: string) => {
+ return useQuery({
+ queryKey: serviceAccountKeys.getServiceAccountById(serviceAccountId),
+ queryFn: () => fetchServiceAccountById(serviceAccountId),
+ enabled: true
+ });
+}
+
+export const useCreateServiceAccount = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post('/api/v2/service-accounts/', body);
+ return data;
+ },
+ onSuccess: ({ serviceAccount }) => {
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(serviceAccount.organization));
+ }
+ });
+}
+
+export const useRenameServiceAccount = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ serviceAccountId, name }) => {
+ const { data: { serviceAccount } } = await apiRequest.patch(`/api/v2/service-accounts/${serviceAccountId}/name`, { name });
+ return serviceAccount;
+ },
+ onSuccess: (serviceAccount) => {
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountById(serviceAccount._id));
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(serviceAccount.organization));
+ }
+ });
+}
+
+export const useDeleteServiceAccount = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (serviceAccountId) => {
+ const { data: { serviceAccount } } = await apiRequest.delete(`/api/v2/service-accounts/${serviceAccountId}`);
+ return serviceAccount;
+ },
+ onSuccess: ({ organization }) => {
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccounts(organization));
+ }
+ });
+}
+
+const fetchServiceAccountProjectLevelPermissions = async (serviceAccountId: string) => {
+ const { data: { serviceAccountWorkspacePermissions } } = await apiRequest.get<{ serviceAccountWorkspacePermissions: ServiceAccountWorkspacePermission[] }>(
+ `/api/v2/service-accounts/${serviceAccountId}/permissions/workspace`
+ );
+
+ return serviceAccountWorkspacePermissions;
+}
+
+export const useGetServiceAccountProjectLevelPermissions = (serviceAccountId: string) => {
+ return useQuery({
+ queryKey: serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccountId),
+ queryFn: () => fetchServiceAccountProjectLevelPermissions(serviceAccountId),
+ enabled: true
+ });
+}
+
+export const useCreateServiceAccountProjectLevelPermission = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data: { serviceAccountWorkspacePermission } } = await apiRequest.post(`/api/v2/service-accounts/${body.serviceAccountId}/permissions/workspace`, body);
+ return serviceAccountWorkspacePermission;
+ },
+ onSuccess: ({ serviceAccount }) => {
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccount));
+ }
+ });
+}
+
+export const useDeleteServiceAccountProjectLevelPermission = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ serviceAccountId, serviceAccountWorkspacePermissionId }) => {
+ const { data: { serviceAccountWorkspacePermission} } = await apiRequest.delete(`/api/v2/service-accounts/${serviceAccountId}/permissions/workspace/${serviceAccountWorkspacePermissionId}`);
+ return serviceAccountWorkspacePermission;
+ },
+ onSuccess: (serviceAccountWorkspacePermission) => {
+ queryClient.invalidateQueries(serviceAccountKeys.getServiceAccountProjectLevelPermissions(serviceAccountWorkspacePermission.serviceAccount));
+ }
+ });
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/serviceAccounts/types.ts b/frontend/src/hooks/api/serviceAccounts/types.ts
new file mode 100644
index 000000000..f8865729b
--- /dev/null
+++ b/frontend/src/hooks/api/serviceAccounts/types.ts
@@ -0,0 +1,51 @@
+import { Workspace } from '../workspace/types';
+
+export type ServiceAccount = {
+ _id: string;
+ name: string;
+ organization: string;
+ user: string;
+ publicKey: string;
+ expiresAt: string;
+}
+
+export type CreateServiceAccountDTO = {
+ name: string;
+ organizationId: string;
+ publicKey: string;
+ expiresIn: number;
+}
+
+export type CreateServiceAccountRes = {
+ serviceAccount: ServiceAccount;
+ serviceAccountAccessKey: string;
+}
+
+export type RenameServiceAccountDTO = {
+ serviceAccountId: string;
+ name: string;
+}
+
+export type ServiceAccountWorkspacePermission = {
+ _id: string;
+ serviceAccount: string;
+ workspace: Workspace;
+ environment: string;
+ read: boolean;
+ write: boolean;
+}
+
+export type CreateServiceAccountWorkspacePermissionDTO = {
+ serviceAccountId: string;
+ workspaceId: string;
+ environment: string;
+ read: boolean;
+ write: boolean;
+ encryptedKey: string;
+ nonce: string;
+}
+
+export type DeleteServiceAccountWorkspacePermissionDTO = {
+ serviceAccountId: string;
+ serviceAccountWorkspacePermissionId: string;
+}
\ No newline at end of file
diff --git a/frontend/src/hooks/api/types.ts b/frontend/src/hooks/api/types.ts
index a8821e5a5..554521828 100644
--- a/frontend/src/hooks/api/types.ts
+++ b/frontend/src/hooks/api/types.ts
@@ -4,6 +4,7 @@ export type { UserWsKeyPair } from './keys/types';
export type { Organization } from './organization/types';
export type { CreateServiceTokenDTO, ServiceToken } from './serviceTokens/types';
export type { GetSubscriptionPlan, SubscriptionPlan } from './subscriptions/types';
+export type { WsTag } from './tags/types';
export type { AddUserToWsDTO, AddUserToWsRes, OrgUser, User } from './users/types';
export type {
CreateEnvironmentDTO,
diff --git a/frontend/src/hooks/api/users/index.tsx b/frontend/src/hooks/api/users/index.tsx
index 0980d4108..d6cfcab2b 100644
--- a/frontend/src/hooks/api/users/index.tsx
+++ b/frontend/src/hooks/api/users/index.tsx
@@ -5,6 +5,8 @@ export {
useDeleteOrgMembership,
useGetOrgUsers,
useGetUser,
+ useGetUserAction,
useLogoutUser,
+ useRegisterUserAction,
useUpdateOrgUserRole
} from './queries';
diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx
index 5d0759755..ef6dcc726 100644
--- a/frontend/src/hooks/api/users/queries.tsx
+++ b/frontend/src/hooks/api/users/queries.tsx
@@ -20,6 +20,7 @@ import {
const userKeys = {
getUser: ['user'] as const,
+ userAction: ['user-action'] as const,
getOrgUsers: (orgId: string) => [{ orgId }, 'user']
};
@@ -31,6 +32,21 @@ const fetchUserDetails = async () => {
export const useGetUser = () => useQuery(userKeys.getUser, fetchUserDetails);
+const fetchUserAction = async (action: string) => {
+ const { data } = await apiRequest.get<{ userAction: string }>('/api/v1/user-action', {
+ params: {
+ action
+ }
+ });
+ return data.userAction;
+};
+
+export const useGetUserAction = (action: string) =>
+ useQuery({
+ queryKey: userKeys.userAction,
+ queryFn: () => fetchUserAction(action)
+ });
+
export const fetchOrgUsers = async (orgId: string) => {
const { data } = await apiRequest.get<{ users: OrgUser[] }>(
`/api/v1/organization/${orgId}/users`
@@ -128,6 +144,16 @@ export const useUpdateOrgUserRole = () => {
});
};
+export const useRegisterUserAction = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, string>({
+ mutationFn: (action) => apiRequest.post('/api/v1/user-action', { action }),
+ onSuccess: () => {
+ queryClient.invalidateQueries(userKeys.userAction);
+ }
+ });
+};
+
export const useLogoutUser = () =>
useMutation({
mutationFn: () => apiRequest.post('/api/v1/auth/logout'),
diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx
index cab8617bd..622c5196c 100644
--- a/frontend/src/hooks/api/workspace/index.tsx
+++ b/frontend/src/hooks/api/workspace/index.tsx
@@ -5,7 +5,9 @@ export {
useDeleteWsEnvironment,
useGetUserWorkspaceMemberships,
useGetUserWorkspaces,
+ useGetUserWsEnvironments,
useGetWorkspaceById,
useRenameWorkspace,
useToggleAutoCapitalization,
- useUpdateWsEnvironment} from './queries';
+ useUpdateWsEnvironment
+} from './queries';
diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx
index 2bca2e9a5..4736da076 100644
--- a/frontend/src/hooks/api/workspace/queries.tsx
+++ b/frontend/src/hooks/api/workspace/queries.tsx
@@ -7,16 +7,19 @@ import {
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
+ GetWsEnvironmentDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
- Workspace
+ Workspace,
+ WorkspaceEnv
} from './types';
const workspaceKeys = {
getWorkspaceById: (workspaceId: string) => [{ workspaceId }, 'workspace'] as const,
getWorkspaceMemberships: (orgId: string) => [{ orgId }, 'workspace-memberships'],
- getAllUserWorkspace: ['workspaces'] as const
+ getAllUserWorkspace: ['workspaces'] as const,
+ getUserWsEnvironments: (workspaceId: string) => ['workspace-env', { workspaceId }] as const
};
const fetchWorkspaceById = async (workspaceId: string) => {
@@ -49,6 +52,21 @@ const fetchUserWorkspaceMemberships = async (orgId: string) => {
return data;
};
+const fetchUserWsEnvironments = async (workspaceId: string) => {
+ const { data } = await apiRequest.get<{ accessibleEnvironments: WorkspaceEnv[] }>(
+ `/api/v2/workspace/${workspaceId}/environments`
+ );
+ return data.accessibleEnvironments;
+};
+
+export const useGetUserWsEnvironments = ({ workspaceId, onSuccess }: GetWsEnvironmentDTO) =>
+ useQuery({
+ enabled: Boolean(workspaceId),
+ onSuccess,
+ queryKey: workspaceKeys.getUserWsEnvironments(workspaceId),
+ queryFn: () => fetchUserWsEnvironments(workspaceId)
+ });
+
// to get all userids in an org with the workspace they are part of
export const useGetUserWorkspaceMemberships = (orgId: string) =>
useQuery({
diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts
index 738f5188c..44d4c4322 100644
--- a/frontend/src/hooks/api/workspace/types.ts
+++ b/frontend/src/hooks/api/workspace/types.ts
@@ -7,7 +7,13 @@ export type Workspace = {
environments: WorkspaceEnv[];
};
-export type WorkspaceEnv = { name: string; slug: string };
+export type WorkspaceEnv = {
+ name: string;
+ slug: string;
+ isReadDenied: boolean;
+ isWriteDenied: boolean;
+};
+
export type WorkspaceTag = { _id: string; name: string; slug: string };
// mutation dto
@@ -16,6 +22,11 @@ export type CreateWorkspaceDTO = {
organizationId: string;
};
+export type GetWsEnvironmentDTO = {
+ workspaceId: string;
+ onSuccess?: (data: WorkspaceEnv[]) => void;
+};
+
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts
index 94bb7abc0..2c2058ef7 100644
--- a/frontend/src/hooks/index.ts
+++ b/frontend/src/hooks/index.ts
@@ -1,3 +1,4 @@
export { useLeaveConfirm } from './useLeaveConfirm';
+export { usePersistentState } from './usePersistentState';
export { usePopUp } from './usePopUp';
export { useToggle } from './useToggle';
diff --git a/frontend/src/hooks/useLeaveConfirm.tsx b/frontend/src/hooks/useLeaveConfirm.tsx
index e136f41e4..3cbba8e19 100644
--- a/frontend/src/hooks/useLeaveConfirm.tsx
+++ b/frontend/src/hooks/useLeaveConfirm.tsx
@@ -4,52 +4,56 @@ import { useRouter } from 'next/router';
import { leaveConfirmDefaultMessage } from '@app/const';
type LeaveConfirmProps = {
- initialValue: boolean,
- message?: string
-}
+ initialValue: boolean;
+ message?: string;
+};
interface LeaveConfirmReturn {
- hasUnsavedChanges: boolean,
- setHasUnsavedChanges: Dispatch>,
+ hasUnsavedChanges: boolean;
+ setHasUnsavedChanges: Dispatch>;
}
export function useLeaveConfirm({
- initialValue,
- message = leaveConfirmDefaultMessage,
+ initialValue,
+ message = leaveConfirmDefaultMessage
}: LeaveConfirmProps): LeaveConfirmReturn {
- const router = useRouter()
+ const router = useRouter();
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(initialValue);
const onRouteChangeStart = useCallback(() => {
if (hasUnsavedChanges) {
+ // eslint-disable-next-line no-alert
if (window.confirm(message)) {
- return true
+ return true;
}
- throw new Error("Abort route change by user's confirmation.")
+ throw new Error("Abort route change by user's confirmation.");
}
return false;
- }, [hasUnsavedChanges])
+ }, [hasUnsavedChanges]);
- const handleWindowClose = useCallback((e: any) => {
- if (!hasUnsavedChanges) {
- return;
- }
- e.preventDefault();
- e.returnValue = message;
- }, []);
+ const handleWindowClose = useCallback(
+ (e: any) => {
+ if (!hasUnsavedChanges) {
+ return;
+ }
+ e.preventDefault();
+ e.returnValue = message;
+ },
+ [hasUnsavedChanges]
+ );
useEffect(() => {
- router.events.on("routeChangeStart", onRouteChangeStart);
+ router.events.on('routeChangeStart', onRouteChangeStart);
window.addEventListener('beforeunload', handleWindowClose);
return () => {
- router.events.off("routeChangeStart", onRouteChangeStart);
+ router.events.off('routeChangeStart', onRouteChangeStart);
window.removeEventListener('beforeunload', handleWindowClose);
- }
+ };
}, [onRouteChangeStart, handleWindowClose]);
return {
hasUnsavedChanges,
- setHasUnsavedChanges,
+ setHasUnsavedChanges
};
}
diff --git a/frontend/src/hooks/usePersistentState.ts b/frontend/src/hooks/usePersistentState.ts
new file mode 100644
index 000000000..0230e35ef
--- /dev/null
+++ b/frontend/src/hooks/usePersistentState.ts
@@ -0,0 +1,25 @@
+import { useEffect, useState } from 'react';
+
+type TPersisntentStateReturn = [T, (val: T) => void];
+
+export const usePersistentState = (
+ initialValue: T,
+ persistenceKey: string
+): TPersisntentStateReturn => {
+ const [val, setVal] = useState(initialValue);
+
+ useEffect(() => {
+ const temp = localStorage.getItem(persistenceKey);
+ if (temp) {
+ const { key } = JSON.parse(temp);
+ setVal(key);
+ }
+ }, []);
+
+ const setState = (state: T) => {
+ localStorage.setItem(persistenceKey, JSON.stringify({ key: state }));
+ setVal(state);
+ };
+
+ return [val, setState];
+};
diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx
index 46b339287..85828c3c5 100644
--- a/frontend/src/layouts/AppLayout/AppLayout.tsx
+++ b/frontend/src/layouts/AppLayout/AppLayout.tsx
@@ -20,6 +20,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { yupResolver } from '@hookform/resolvers/yup';
+import queryString from 'query-string';
import * as yup from 'yup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
@@ -93,8 +94,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
// Placing the localstorage as much as possible
// Wait till tony integrates the azure and its launched
useEffect(() => {
-
// Put a user in a workspace if they're not in one yet
+
const putUserInWorkSpace = async () => {
if (tempLocalStorage('orgData.id') === '') {
const userOrgs = await getOrganizations();
@@ -114,9 +115,38 @@ export const AppLayout = ({ children }: LayoutProps) => {
) {
router.push('/noprojects');
} else if (router.asPath !== '/noprojects') {
- const intendedWorkspaceId = router.asPath
- .split('/')
- [router.asPath.split('/').length - 1].split('?')[0];
+
+ // const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
+
+ // let intendedWorkspaceId;
+ // if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
+ // intendedWorkspaceId = pathSegments[1];
+ // } else if (pathSegments.length >= 3 && pathSegments[0] === 'settings') {
+ // intendedWorkspaceId = pathSegments[2];
+ // } else {
+ // intendedWorkspaceId = router.asPath
+ // .split('/')
+ // [router.asPath.split('/').length - 1].split('?')[0];
+ // }
+
+ const pathSegments = router.asPath.split('/').filter(segment => segment.length > 0);
+
+ let intendedWorkspaceId;
+ if (pathSegments.length >= 2 && pathSegments[0] === 'dashboard') {
+ [, intendedWorkspaceId] = pathSegments;
+ } else if (pathSegments.length >= 3 && pathSegments[0] === 'settings') {
+ [, , intendedWorkspaceId] = pathSegments;
+ } else {
+ const lastPathSegments = router.asPath.split('/').pop();
+ if (lastPathSegments !== undefined) {
+ [intendedWorkspaceId] = lastPathSegments.split('?');
+ }
+
+ // const lastPathSegment = router.asPath.split('/').pop().split('?');
+ // [intendedWorkspaceId] = lastPathSegment;
+ }
+
+ if (!intendedWorkspaceId) return;
if (!['callback', 'create', 'authorize'].includes(intendedWorkspaceId)) {
localStorage.setItem('projectData.id', intendedWorkspaceId);
@@ -129,7 +159,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
.map((workspace: { _id: string }) => workspace._id)
.includes(intendedWorkspaceId)
) {
- router.push(`/dashboard/${userWorkspaces[0]._id}`);
+ const { env } = queryString.parse(router.asPath.split('?')[1]);
+ if (!env) {
+ router.push(`/dashboard/${userWorkspaces[0]._id}`);
+ }
} else {
setWorkspaceMapping(
Object.fromEntries(
@@ -192,7 +225,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
});
if (addMembers) {
- console.log('adding other users');
// not using hooks because need at this point only
const orgUsers = await fetchOrgUsers(currentOrg._id);
orgUsers.forEach(({ status, user: orgUser }) => {
@@ -243,11 +275,12 @@ export const AppLayout = ({ children }: LayoutProps) => {
{name}
))}
-
+ {/* */}
handlePopUpOpen('addNewWs')}
leftIcon={ }
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 2b5dd0fc2..30d2623b5 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -8,6 +8,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import NotificationProvider from '@app/components/context/Notifications/NotificationProvider';
import Telemetry from '@app/components/utilities/telemetry/Telemetry';
+import { TooltipProvider } from '@app/components/v2';
import { publicPaths } from '@app/const';
import {
AuthProvider,
@@ -73,21 +74,23 @@ const App = ({ Component, pageProps, ...appProps }: NextAppProp): JSX.Element =>
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/frontend/src/pages/activity/[id].tsx b/frontend/src/pages/activity/[id].tsx
index 8e16fccd7..c43a6745d 100644
--- a/frontend/src/pages/activity/[id].tsx
+++ b/frontend/src/pages/activity/[id].tsx
@@ -20,6 +20,12 @@ interface LogData {
user: {
email: string;
};
+ serviceAccount?: {
+ string: string;
+ },
+ serviceTokenData?: {
+ name: string;
+ }
actions: {
_id: string;
name: string;
@@ -41,6 +47,12 @@ interface LogDataPoint {
createdAt: string;
ipAddress: string;
user: string;
+ serviceAccount: {
+ name: string;
+ };
+ serviceTokenData: {
+ name: string;
+ };
payload: PayloadProps[];
}
@@ -69,13 +81,16 @@ export default function Activity() {
userId: '',
actionNames: eventChosen
});
+
setLogsData(
tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
- user: log.user.email,
+ user: log?.user?.email,
+ serviceAccount: log?.serviceAccount,
+ serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
@@ -106,7 +121,9 @@ export default function Activity() {
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
- user: log.user.email,
+ user: log?.user?.email,
+ serviceAccount: log?.serviceAccount,
+ serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
diff --git a/frontend/src/pages/api/integrations/DeleteIntegration.ts b/frontend/src/pages/api/integrations/DeleteIntegration.ts
index a16c629a3..ae401984c 100644
--- a/frontend/src/pages/api/integrations/DeleteIntegration.ts
+++ b/frontend/src/pages/api/integrations/DeleteIntegration.ts
@@ -19,7 +19,6 @@ const deleteIntegration = ({ integrationId }: Props) =>
if (res && res.status === 200) {
return (await res.json()).integration;
}
- console.log('Failed to delete an integration');
return undefined;
});
diff --git a/frontend/src/pages/api/integrations/createIntegration.ts b/frontend/src/pages/api/integrations/createIntegration.ts
index d861223f1..e3e7c010a 100644
--- a/frontend/src/pages/api/integrations/createIntegration.ts
+++ b/frontend/src/pages/api/integrations/createIntegration.ts
@@ -7,6 +7,9 @@ interface Props {
appId: string | null;
sourceEnvironment: string;
targetEnvironment: string | null;
+ targetEnvironmentId: string | null;
+ targetService: string | null;
+ targetServiceId: string | null;
owner: string | null;
path: string | null;
region: string | null;
@@ -24,6 +27,9 @@ const createIntegration = ({
appId,
sourceEnvironment,
targetEnvironment,
+ targetEnvironmentId,
+ targetService,
+ targetServiceId,
owner,
path,
region
@@ -40,6 +46,9 @@ const createIntegration = ({
appId,
sourceEnvironment,
targetEnvironment,
+ targetEnvironmentId,
+ targetService,
+ targetServiceId,
owner,
path,
region
diff --git a/frontend/src/pages/api/workspace/getLatestFileKey.ts b/frontend/src/pages/api/workspace/getLatestFileKey.ts
index 576fcf943..f649987db 100644
--- a/frontend/src/pages/api/workspace/getLatestFileKey.ts
+++ b/frontend/src/pages/api/workspace/getLatestFileKey.ts
@@ -1,22 +1,13 @@
-import SecurityClient from '@app/components/utilities/SecurityClient';
+import { apiRequest } from '@app/config/request';
/**
* Get the latest key pairs from a certain workspace
* @param {string} workspaceId
* @returns
*/
-const getLatestFileKey = ({ workspaceId }: { workspaceId: string }) =>
- SecurityClient.fetchCall(`/api/v1/key/${workspaceId}/latest`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(async (res) => {
- if (res?.status === 200) {
- return res.json();
- }
- console.log('Failed to get the latest key pairs for a certain project');
- return undefined;
- });
+const getLatestFileKey = async ({ workspaceId }: { workspaceId: string }) => {
+ const { data } = await apiRequest.get(`/api/v1/key/${workspaceId}/latest`);
+ return data;
+}
export default getLatestFileKey;
diff --git a/frontend/src/pages/dashboard/[id].tsx b/frontend/src/pages/dashboard/[id].tsx
index 799c05c39..8aa480c81 100644
--- a/frontend/src/pages/dashboard/[id].tsx
+++ b/frontend/src/pages/dashboard/[id].tsx
@@ -19,9 +19,9 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tag } from 'public/data/frequentInterfaces';
+import queryString from 'query-string';
import Button from '@app/components/basic/buttons/Button';
-import ListBox from '@app/components/basic/Listbox';
import BottonRightPopup from '@app/components/basic/popups/BottomRightPopup';
import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
import ConfirmEnvOverwriteModal from '@app/components/dashboard/ConfirmEnvOverwriteModal';
@@ -41,6 +41,8 @@ import getProjectSercetSnapshotsCount from '@app/ee/api/secrets/GetProjectSercet
import performSecretRollback from '@app/ee/api/secrets/PerformSecretRollback';
import PITRecoverySidebar from '@app/ee/components/PITRecoverySidebar';
import { useLeaveConfirm } from '@app/hooks';
+// import { DashboardPage } from '@app/views/DashboardPage';
+import { DashboardEnvOverview } from '@app/views/DashboardPage/DashboardEnvOverview';
// import addSecrets from '../api/files/AddSecrets';
// import deleteSecrets from '../api/files/DeleteSecrets';
@@ -124,7 +126,7 @@ interface SecretProps {
}
/**
- * this function finds the teh duplicates in an array
+ * this function finds the the duplicates in an array
* @param arr - array of anything (e.g., with secret keys and types (personal/shared))
* @returns - a list with duplicates
*/
@@ -147,7 +149,6 @@ function findDuplicates(arr: any[]) {
export default function Dashboard() {
const [data, setData] = useState();
const [initialData, setInitialData] = useState([]);
- const router = useRouter();
const [blurred, setBlurred] = useState(true);
const [isKeyAvailable, setIsKeyAvailable] = useState(true);
const [isNew, setIsNew] = useState(false);
@@ -169,7 +170,10 @@ export default function Dashboard() {
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
const { t } = useTranslation();
+
const { createNotification } = useNotificationContext();
+ const router = useRouter();
+ const envInURL = queryString.parse(router.asPath.split('?')[1])?.env;
const workspaceId = router.query.id as string;
const [workspaceEnvs, setWorkspaceEnvs] = useState([]);
@@ -783,13 +787,13 @@ export default function Dashboard() {
deleteRow({ ids, secretName });
};
- const handleOnEnvironmentChange = (envName: string) => {
+ const handleOnEnvironmentChange = (envSlug: string) => {
if (hasUnsavedChanges) {
if (!window.confirm(leaveConfirmDefaultMessage)) return;
}
const selectedWorkspaceEnv = workspaceEnvs.find(
- ({ name }: { name: string }) => envName === name
+ ({ slug }: { slug: string }) => envSlug === slug
) || {
name: 'unknown',
slug: 'unknown',
@@ -797,6 +801,8 @@ export default function Dashboard() {
isReadDenied: false
};
+ console.log(124, envSlug, selectedWorkspaceEnv)
+
if (selectedWorkspaceEnv) {
if (snapshotData) setSelectedSnapshotEnv(selectedWorkspaceEnv);
else setSelectedEnv(selectedWorkspaceEnv);
@@ -809,7 +815,10 @@ export default function Dashboard() {
})
};
- return data ? (
+ return
+ {!envInURL
+ ?
+ : (data ? (
{t('common:head-title', { title: t('dashboard:title') })}
@@ -835,7 +844,13 @@ export default function Dashboard() {
}
/>
-
+
envir.slug === envInURL)[0].name || ''}
+ isProjectRelated
+ userAvailableEnvs={workspaceEnvs}
+ onEnvChange={handleOnEnvironmentChange}
+ />
{checkDocsPopUpVisible && (
setSnapshotData(undefined)}
color="mineshaft"
size="md"
@@ -863,7 +878,7 @@ export default function Dashboard() {
{snapshotData ? 'Secret Snapshot' : t('dashboard:title')}
{snapshotData && (
-
+
{new Date(snapshotData.createdAt).toLocaleString()}
)}
@@ -873,13 +888,6 @@ export default function Dashboard() {
)}
- {!snapshotData && data?.length === 0 && selectedEnv && (
- name)}
- onChange={handleOnEnvironmentChange}
- />
- )}
@@ -960,20 +968,7 @@ export default function Dashboard() {
{(snapshotData || data?.length !== 0) && selectedEnv && (
<>
- {!snapshotData ? (
-
name)}
- onChange={handleOnEnvironmentChange}
- />
- ) : (
- name)}
- onChange={handleOnEnvironmentChange}
- />
- )}
-
+
@@ -1223,10 +1218,10 @@ export default function Dashboard() {
) : (
-
}
Dashboard.requireAuth = true;
diff --git a/frontend/src/pages/integrations/[id].tsx b/frontend/src/pages/integrations/[id].tsx
index c3c625a79..46870805d 100644
--- a/frontend/src/pages/integrations/[id].tsx
+++ b/frontend/src/pages/integrations/[id].tsx
@@ -207,6 +207,12 @@ export default function Integrations() {
case 'travisci':
link = `${window.location.origin}/integrations/travisci/authorize`;
break;
+ case 'supabase':
+ link = `${window.location.origin}/integrations/supabase/authorize`;
+ break;
+ case 'railway':
+ link = `${window.location.origin}/integrations/railway/authorize`;
+ break;
default:
break;
}
@@ -259,6 +265,12 @@ export default function Integrations() {
case 'travisci':
link = `${window.location.origin}/integrations/travisci/create?integrationAuthId=${integrationAuth._id}`;
break;
+ case 'supabase':
+ link = `${window.location.origin}/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`;
+ break;
+ case 'railway':
+ link = `${window.location.origin}/integrations/railway/create?integrationAuthId=${integrationAuth._id}`;
+ break;
default:
break;
}
diff --git a/frontend/src/pages/integrations/aws-parameter-store/create.tsx b/frontend/src/pages/integrations/aws-parameter-store/create.tsx
index 624f5937a..200628921 100644
--- a/frontend/src/pages/integrations/aws-parameter-store/create.tsx
+++ b/frontend/src/pages/integrations/aws-parameter-store/create.tsx
@@ -98,6 +98,9 @@ export default function AWSParameterStoreCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path,
region: selectedAWSRegion
diff --git a/frontend/src/pages/integrations/aws-secret-manager/create.tsx b/frontend/src/pages/integrations/aws-secret-manager/create.tsx
index 804cf0504..7f253e119 100644
--- a/frontend/src/pages/integrations/aws-secret-manager/create.tsx
+++ b/frontend/src/pages/integrations/aws-secret-manager/create.tsx
@@ -97,6 +97,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: selectedAWSRegion
diff --git a/frontend/src/pages/integrations/azure-key-vault/create.tsx b/frontend/src/pages/integrations/azure-key-vault/create.tsx
index bfd544748..8f506b6d4 100644
--- a/frontend/src/pages/integrations/azure-key-vault/create.tsx
+++ b/frontend/src/pages/integrations/azure-key-vault/create.tsx
@@ -62,6 +62,9 @@ export default function AzureKeyVaultCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null
diff --git a/frontend/src/pages/integrations/circleci/create.tsx b/frontend/src/pages/integrations/circleci/create.tsx
index b753e7dd7..05a53e609 100644
--- a/frontend/src/pages/integrations/circleci/create.tsx
+++ b/frontend/src/pages/integrations/circleci/create.tsx
@@ -60,6 +60,9 @@ export default function CircleCICreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null,
diff --git a/frontend/src/pages/integrations/flyio/create.tsx b/frontend/src/pages/integrations/flyio/create.tsx
index 05b37affd..13dbc05d1 100644
--- a/frontend/src/pages/integrations/flyio/create.tsx
+++ b/frontend/src/pages/integrations/flyio/create.tsx
@@ -61,6 +61,9 @@ export default function FlyioCreateIntegrationPage() {
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null
diff --git a/frontend/src/pages/integrations/github/create.tsx b/frontend/src/pages/integrations/github/create.tsx
index 5fbd8f780..eee9f8a71 100644
--- a/frontend/src/pages/integrations/github/create.tsx
+++ b/frontend/src/pages/integrations/github/create.tsx
@@ -27,8 +27,7 @@ export default function GitHubCreateIntegrationPage() {
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
- const [owner, setOwner] = useState(null);
- const [targetApp, setTargetApp] = useState('');
+ const [targetAppId, setTargetAppId] = useState('');
const [isLoading, setIsLoading] = useState(false);
@@ -41,10 +40,9 @@ export default function GitHubCreateIntegrationPage() {
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
- setTargetApp(integrationAuthApps[0].name);
- setOwner(integrationAuthApps[0]?.owner ?? null);
+ setTargetAppId(integrationAuthApps[0].appId as string);
} else {
- setTargetApp('none');
+ setTargetAppId('none');
}
}
}, [integrationAuthApps]);
@@ -55,14 +53,21 @@ export default function GitHubCreateIntegrationPage() {
if (!integrationAuth?._id) return;
+ const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId);
+
+ if (!targetApp || !targetApp.owner) return;
+
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
- app: targetApp,
+ app: targetApp.name,
appId: null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
- owner,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
+ owner: targetApp.owner,
path: null,
region: null
});
@@ -76,7 +81,7 @@ export default function GitHubCreateIntegrationPage() {
}
}
- return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp) ? (
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetAppId) ? (
GitHub Integration
@@ -101,14 +106,14 @@ export default function GitHubCreateIntegrationPage() {
className='mt-4'
>
setTargetApp(val)}
+ value={targetAppId}
+ onValueChange={(val) => setTargetAppId(val)}
className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
-
+
{integrationAuthApp.name}
))
diff --git a/frontend/src/pages/integrations/gitlab/create.tsx b/frontend/src/pages/integrations/gitlab/create.tsx
index 259285109..c8a25e78b 100644
--- a/frontend/src/pages/integrations/gitlab/create.tsx
+++ b/frontend/src/pages/integrations/gitlab/create.tsx
@@ -89,6 +89,9 @@ export default function GitLabCreateIntegrationPage() {
appId: targetAppId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null
@@ -109,7 +112,6 @@ export default function GitLabCreateIntegrationPage() {
GitLab Integration
integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null
diff --git a/frontend/src/pages/integrations/railway/authorize.tsx b/frontend/src/pages/integrations/railway/authorize.tsx
new file mode 100644
index 000000000..94d3f06bf
--- /dev/null
+++ b/frontend/src/pages/integrations/railway/authorize.tsx
@@ -0,0 +1,77 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Input,
+} from '../../../components/v2';
+import saveIntegrationAccessToken from "../../api/integrations/saveIntegrationAccessToken";
+
+export default function RailwayAuthorizeIntegrationPage() {
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState('');
+ const [apiKeyErrorText, setApiKeyErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setApiKeyErrorText('');
+ if (apiKey.length === 0) {
+ setApiKeyErrorText('API Key cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'railway',
+ accessId: null,
+ accessToken: apiKey
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/railway/create?integrationAuthId=${integrationAuth._id}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+
+ Railway Integration
+
+ setApiKey(e.target.value)}
+ />
+
+
+ Connect to Railway
+
+
+
+ );
+}
+
+RailwayAuthorizeIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/railway/create.tsx b/frontend/src/pages/integrations/railway/create.tsx
new file mode 100644
index 000000000..95eb90c6f
--- /dev/null
+++ b/frontend/src/pages/integrations/railway/create.tsx
@@ -0,0 +1,207 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import {
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
+} from '../../../components/v2';
+import {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById,
+ useGetIntegrationAuthRailwayEnvironments,
+ useGetIntegrationAuthRailwayServices
+} from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from "../../api/integrations/createIntegration";
+
+export default function RailwayCreateIntegrationPage() {
+ const router = useRouter();
+
+ const [targetAppId, setTargetAppId] = useState('');
+ const [targetEnvironmentId, setTargetEnvironmentId] = useState('');
+ const [targetServiceId, setTargetServiceId] = useState('');
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+ const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps({
+ integrationAuthId: integrationAuthId as string ?? ''
+ });
+ const { data: targetEnvironments } = useGetIntegrationAuthRailwayEnvironments({
+ integrationAuthId: integrationAuthId as string ?? '',
+ appId: targetAppId
+ });
+ const { data: targetServices } = useGetIntegrationAuthRailwayServices({
+ integrationAuthId: integrationAuthId as string ?? '',
+ appId: targetAppId
+ });
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ if (integrationAuthApps) {
+ if (integrationAuthApps.length > 0) {
+ setTargetAppId(integrationAuthApps[0].appId as string);
+ } else {
+ setTargetAppId('none');
+ }
+ }
+ }, [integrationAuthApps]);
+
+ useEffect(() => {
+ if (targetEnvironments) {
+ if (targetEnvironments.length > 0) {
+ setTargetEnvironmentId(targetEnvironments[0].environmentId);
+ } else {
+ setTargetEnvironmentId('none');
+ }
+ }
+ }, [targetEnvironments]);
+
+ const filteredServices = targetServices
+ ?.concat({
+ name: '',
+ serviceId: ''
+ });
+
+ const handleButtonClick = async () => {
+ try {
+ setIsLoading(true);
+
+ if (!integrationAuth?._id) return;
+
+ const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId);
+ const targetEnvironment = targetEnvironments?.find((environment) => environment.environmentId === targetEnvironmentId);
+
+ if (!targetApp || !targetApp.appId || !targetEnvironment) return;
+
+ const targetService = targetServices?.find((service) => service.serviceId === targetServiceId);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp.name,
+ appId: targetApp.appId,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: targetEnvironment.name,
+ targetEnvironmentId: targetEnvironment.environmentId,
+ targetService: targetService ? targetService.name : null,
+ targetServiceId: targetService ? targetService.serviceId : null,
+ owner: null,
+ path: null,
+ region: null
+ });
+
+ setIsLoading(false);
+
+ router.push(
+ `/integrations/${localStorage.getItem('projectData.id')}`
+ );
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return workspace && selectedSourceEnvironment && integrationAuthApps && targetEnvironments && filteredServices ? (
+
+
+ Railway Integration
+
+ setSelectedSourceEnvironment(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetAppId(val)}
+ className='w-full border border-mineshaft-500'
+ isDisabled={integrationAuthApps.length === 0}
+ >
+ {integrationAuthApps.length > 0 ? (
+ integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))
+ ) : (
+
+ No projects found
+
+ )}
+
+
+
+ setTargetEnvironmentId(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {targetEnvironments.length > 0 ? (
+ targetEnvironments.map((targetEnvironment) => (
+
+ {targetEnvironment.name}
+
+ ))
+ ) : (
+
+ No environments found
+
+ )}
+
+
+
+ setTargetServiceId(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {filteredServices.map((targetService) => (
+
+ {targetService.name}
+
+ ))}
+
+
+
+ Create Integration
+
+
+
+ ) :
+}
+
+RailwayCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
+
diff --git a/frontend/src/pages/integrations/render/create.tsx b/frontend/src/pages/integrations/render/create.tsx
index cc61eec3e..ce9d702ee 100644
--- a/frontend/src/pages/integrations/render/create.tsx
+++ b/frontend/src/pages/integrations/render/create.tsx
@@ -11,7 +11,7 @@ import {
Select,
SelectItem
} from '../../../components/v2';
-import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import { useGetIntegrationAuthApps, useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
@@ -38,7 +38,6 @@ export default function RenderCreateIntegrationPage() {
}, [workspace]);
useEffect(() => {
- // TODO: handle case where apps can be empty
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
@@ -61,6 +60,9 @@ export default function RenderCreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null
diff --git a/frontend/src/pages/integrations/supabase/authorize.tsx b/frontend/src/pages/integrations/supabase/authorize.tsx
new file mode 100644
index 000000000..95785be6f
--- /dev/null
+++ b/frontend/src/pages/integrations/supabase/authorize.tsx
@@ -0,0 +1,65 @@
+import { useState } from 'react';
+import { useRouter } from 'next/router';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import { Button, Card, CardTitle, FormControl, Input } from '../../../components/v2';
+import saveIntegrationAccessToken from '../../api/integrations/saveIntegrationAccessToken';
+
+export default function SupabaseCreateIntegrationPage() {
+ const router = useRouter();
+ const [apiKey, setApiKey] = useState('');
+ const [apiKeyErrorText, setApiKeyErrorText] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleButtonClick = async () => {
+ try {
+ setApiKeyErrorText('');
+ if (apiKey.length === 0) {
+ setApiKeyErrorText('API Key cannot be blank');
+ return;
+ }
+
+ setIsLoading(true);
+
+ const integrationAuth = await saveIntegrationAccessToken({
+ workspaceId: localStorage.getItem('projectData.id'),
+ integration: 'supabase',
+ accessToken: apiKey,
+ accessId: null
+ });
+
+ setIsLoading(false);
+
+ router.push(`/integrations/supabase/create?integrationAuthId=${integrationAuth._id}`);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return (
+
+
+ Supabase Integration
+
+ setApiKey(e.target.value)} />
+
+
+ Connect to Supabase
+
+
+
+ );
+}
+
+SupabaseCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/supabase/create.tsx b/frontend/src/pages/integrations/supabase/create.tsx
new file mode 100644
index 000000000..16e35e36e
--- /dev/null
+++ b/frontend/src/pages/integrations/supabase/create.tsx
@@ -0,0 +1,142 @@
+import { useEffect, useState } from 'react';
+import { useRouter } from 'next/router';
+import queryString from 'query-string';
+
+import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
+import { Button, Card, CardTitle, FormControl, Select, SelectItem } from '../../../components/v2';
+import {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById
+} from '../../../hooks/api/integrationAuth';
+import { useGetWorkspaceById } from '../../../hooks/api/workspace';
+import createIntegration from '../../api/integrations/createIntegration';
+
+export default function SupabaseCreateIntegrationPage() {
+ const router = useRouter();
+
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+
+ const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
+ const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? '');
+ const { data: integrationAuthApps } = useGetIntegrationAuthApps({
+ integrationAuthId: (integrationAuthId as string) ?? ''
+ });
+
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetApp, setTargetApp] = useState('');
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ if (workspace) {
+ setSelectedSourceEnvironment(workspace.environments[0].slug);
+ }
+ }, [workspace]);
+
+ useEffect(() => {
+ if (integrationAuthApps) {
+ if (integrationAuthApps.length > 0) {
+ setTargetApp(integrationAuthApps[0].name);
+ } else {
+ setTargetApp('none');
+ }
+ }
+ }, [integrationAuthApps]);
+
+ const handleButtonClick = async () => {
+ try {
+ if (!integrationAuth?._id) return;
+
+ setIsLoading(true);
+
+ await createIntegration({
+ integrationAuthId: integrationAuth?._id,
+ isActive: true,
+ app: targetApp,
+ appId:
+ integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp)
+ ?.appId ?? null,
+ sourceEnvironment: selectedSourceEnvironment,
+ targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
+ owner: null,
+ path: null,
+ region: null
+ });
+
+ setIsLoading(false);
+
+ router.push(`/integrations/${localStorage.getItem('projectData.id')}`);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return integrationAuth &&
+ workspace &&
+ selectedSourceEnvironment &&
+ integrationAuthApps &&
+ targetApp ? (
+
+
+ Supabase Integration
+
+ setSelectedSourceEnvironment(val)}
+ className="w-full border border-mineshaft-500"
+ >
+ {workspace?.environments.map((sourceEnvironment) => (
+
+ {sourceEnvironment.name}
+
+ ))}
+
+
+
+ setTargetApp(val)}
+ className="w-full border border-mineshaft-500"
+ isDisabled={integrationAuthApps.length === 0}
+ >
+ {integrationAuthApps.length > 0 ? (
+ integrationAuthApps.map((integrationAuthApp) => (
+
+ {integrationAuthApp.name}
+
+ ))
+ ) : (
+
+ No projects found
+
+ )}
+
+
+
+ Create Integration
+
+
+
+ ) : (
+
+ );
+}
+
+SupabaseCreateIntegrationPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps(['integrations']);
\ No newline at end of file
diff --git a/frontend/src/pages/integrations/travisci/create.tsx b/frontend/src/pages/integrations/travisci/create.tsx
index c4a8a8f95..7f4e93422 100644
--- a/frontend/src/pages/integrations/travisci/create.tsx
+++ b/frontend/src/pages/integrations/travisci/create.tsx
@@ -60,6 +60,9 @@ export default function TravisCICreateIntegrationPage() {
appId: (integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.name === targetApp))?.appId ?? null,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment: null,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
path: null,
region: null,
diff --git a/frontend/src/pages/integrations/vercel/create.tsx b/frontend/src/pages/integrations/vercel/create.tsx
index 6431b4597..531033957 100644
--- a/frontend/src/pages/integrations/vercel/create.tsx
+++ b/frontend/src/pages/integrations/vercel/create.tsx
@@ -4,14 +4,18 @@ import queryString from 'query-string';
import { getTranslatedServerSideProps } from '../../../components/utilities/withTranslateProps';
import {
- Button,
- Card,
- CardTitle,
- FormControl,
- Select,
- SelectItem
+ Button,
+ Card,
+ CardTitle,
+ FormControl,
+ Select,
+ SelectItem
} from '../../../components/v2';
-import { useGetIntegrationAuthApps,useGetIntegrationAuthById } from '../../../hooks/api/integrationAuth';
+import {
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthById,
+ useGetIntegrationAuthVercelBranches
+} from '../../../hooks/api/integrationAuth';
import { useGetWorkspaceById } from '../../../hooks/api/workspace';
import createIntegration from "../../api/integrations/createIntegration";
@@ -24,20 +28,28 @@ const vercelEnvironments = [
export default function VercelCreateIntegrationPage() {
const router = useRouter();
- const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
+ const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
+ const [targetAppId, setTargetAppId] = useState('');
+ const [targetEnvironment, setTargetEnvironment] = useState('');
+ const [targetBranch, setTargetBranch] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const { integrationAuthId } = queryString.parse(router.asPath.split('?')[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem('projectData.id') ?? '');
const { data: integrationAuth } = useGetIntegrationAuthById(integrationAuthId as string ?? '');
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: integrationAuthId as string ?? ''
});
- const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState('');
- const [targetApp, setTargetApp] = useState('');
- const [targetEnvironment, setTargetEnvironment] = useState('');
-
- const [isLoading, setIsLoading] = useState(false);
+ const { data: branches } = useGetIntegrationAuthVercelBranches({
+ integrationAuthId: integrationAuthId as string,
+ appId: targetAppId,
+ });
+ const filteredBranches = branches
+ ?.filter((branchName) => branchName !== 'main')
+ .concat('');
+
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
@@ -45,15 +57,15 @@ export default function VercelCreateIntegrationPage() {
}, [workspace]);
useEffect(() => {
- if (integrationAuthApps) {
- if (integrationAuthApps.length > 0) {
- setTargetApp(integrationAuthApps[0].name);
- setTargetEnvironment(vercelEnvironments[0].slug);
- } else {
- setTargetApp('none');
- setTargetEnvironment(vercelEnvironments[0].slug);
- }
+ if (integrationAuthApps) {
+ if (integrationAuthApps.length > 0) {
+ setTargetAppId(integrationAuthApps[0].appId as string);
+ setTargetEnvironment(vercelEnvironments[0].slug);
+ } else {
+ setTargetAppId('none');
+ setTargetEnvironment(vercelEnvironments[0].slug);
}
+ }
}, [integrationAuthApps]);
const handleButtonClick = async () => {
@@ -61,15 +73,25 @@ export default function VercelCreateIntegrationPage() {
if (!integrationAuth?._id) return;
setIsLoading(true);
+
+ const targetApp = integrationAuthApps?.find((integrationAuthApp) => integrationAuthApp.appId === targetAppId);
+
+ if (!targetApp || !targetApp.appId) return;
+
+ const path = (targetEnvironment === 'preview' && targetBranch !== '') ? targetBranch : null;
+
await createIntegration({
integrationAuthId: integrationAuth?._id,
isActive: true,
- app: targetApp,
- appId: null,
+ app: targetApp.name,
+ appId: targetApp.appId,
sourceEnvironment: selectedSourceEnvironment,
targetEnvironment,
+ targetEnvironmentId: null,
+ targetService: null,
+ targetServiceId: null,
owner: null,
- path: null,
+ path,
region: null
});
@@ -82,7 +104,7 @@ export default function VercelCreateIntegrationPage() {
}
}
- return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetApp && targetEnvironment) ? (
+ return (integrationAuth && workspace && selectedSourceEnvironment && integrationAuthApps && targetAppId && targetEnvironment) ? (
Vercel Integration
@@ -106,14 +128,14 @@ export default function VercelCreateIntegrationPage() {
label="Vercel App"
>
setTargetApp(val)}
+ value={targetAppId}
+ onValueChange={(val) => setTargetAppId(val)}
className='w-full border border-mineshaft-500'
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
-
+
{integrationAuthApp.name}
))
@@ -139,6 +161,23 @@ export default function VercelCreateIntegrationPage() {
))}
+ {targetEnvironment === 'preview' && filteredBranches && (
+
+ setTargetBranch(val)}
+ className='w-full border border-mineshaft-500'
+ >
+ {filteredBranches.map((branchName) => (
+
+ {branchName}
+
+ ))}
+
+
+ )}
+
+ Edit Service Account
+
+
+
+ >
+ );
+}
+
+ServiceAccountPage.requireAuth = true;
+
+export const getServerSideProps = getTranslatedServerSideProps([
+ 'settings',
+ 'settings-org',
+ 'section-incident',
+ 'section-members'
+]);
\ No newline at end of file
diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css
index df32da8d0..feca862f6 100644
--- a/frontend/src/styles/globals.css
+++ b/frontend/src/styles/globals.css
@@ -2,6 +2,42 @@
@tailwind components;
@tailwind utilities;
+@layer utilities {
+ .flex-0 {
+ flex:0;
+ }
+ .flex-2 {
+ flex-grow: 2;
+ }
+
+ .flex-3 {
+ flex-grow: 3;
+ }
+}
+
+@layer components {
+ .secret-table {
+ @apply bg-mineshaft-800 text-left text-bunker-300 w-full;
+ }
+
+ /* padding except for comment column */
+ .secret-table th {
+ @apply py-2 px-4 font-medium;
+ }
+
+ .secret-table td {
+ @apply py-1 px-1 pr-2 text-sm;
+ }
+
+ .secret-table th:not(:last-child),.secret-table td:not(:last-child) {
+ @apply border-r border-mineshaft-600;
+ }
+
+ .secret-table tr {
+ @apply border-b border-mineshaft-600;
+ }
+}
+
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
-@import '@fontsource/inter/700.css';
\ No newline at end of file
+@import '@fontsource/inter/700.css';
diff --git a/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx
new file mode 100644
index 000000000..a7545fb1c
--- /dev/null
+++ b/frontend/src/views/DashboardPage/DashboardEnvOverview.tsx
@@ -0,0 +1,275 @@
+import { useEffect, useState } from 'react';
+import { FormProvider, useForm, useWatch } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { useRouter } from 'next/router';
+import { yupResolver } from '@hookform/resolvers/yup';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import NavHeader from '@app/components/navigation/NavHeader';
+import {
+ Button,
+ Modal,
+ ModalContent,
+ TableContainer,
+ Tooltip
+} from '@app/components/v2';
+import { useWorkspace } from '@app/context';
+import { usePopUp } from '@app/hooks';
+import {
+ useCreateWsTag,
+ useGetProjectSecrets,
+ useGetUserWsEnvironments,
+ useGetUserWsKey,
+} from '@app/hooks/api';
+import { WorkspaceEnv } from '@app/hooks/api/types';
+
+import { CreateTagModal } from './components/CreateTagModal';
+import { EnvComparisonRow } from './components/EnvComparisonRow';
+import {
+ FormData,
+ schema
+} from './DashboardPage.utils';
+
+
+export const DashboardEnvOverview = () => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { createNotification } = useNotificationContext();
+
+ const { popUp
+ // , handlePopUpOpen
+ , handlePopUpToggle, handlePopUpClose } = usePopUp([
+ 'secretDetails',
+ 'addTag',
+ 'secretSnapshots',
+ 'uploadedSecOpts',
+ 'compareSecrets'
+ ] as const);
+ const [selectedEnv, setSelectedEnv] = useState(null);
+
+ const { currentWorkspace, isLoading } = useWorkspace();
+ const workspaceId = currentWorkspace?._id as string;
+ const { data: latestFileKey } = useGetUserWsKey(workspaceId);
+
+ useEffect(() => {
+ if (!isLoading && !workspaceId && router.isReady) {
+ router.push('/noprojects');
+ }
+ }, [isLoading, workspaceId, router.isReady]);
+
+ const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
+ workspaceId,
+ onSuccess: (data) => {
+ // get an env with one of the access available
+ const env = data.find(({ isReadDenied }) => !isReadDenied);
+ if (env) {
+ setSelectedEnv(env);
+ }
+ }
+ });
+
+ const userAvailableEnvs = wsEnv?.filter(
+ ({ isReadDenied }) => !isReadDenied
+ );
+
+ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
+ workspaceId,
+ env: userAvailableEnvs?.map(env => env.slug) ?? [],
+ decryptFileKey: latestFileKey!,
+ isPaused: false
+ });
+
+ // mutation calls
+ const { mutateAsync: createWsTag } = useCreateWsTag();
+
+ const method = useForm({
+ // why any: well yup inferred ts expects other keys to defined as undefined
+ defaultValues: secrets as any,
+ values: secrets as any,
+ mode: 'onBlur',
+ resolver: yupResolver(schema)
+ });
+
+ const {
+ control,
+ // handleSubmit,
+ // getValues,
+ // setValue,
+ // formState: { isSubmitting, dirtyFields },
+ // reset
+ } = method;
+ const formSecrets = useWatch({ control, name: 'secrets' });
+
+ const isReadOnly = selectedEnv?.isWriteDenied;
+
+ const onCreateWsTag = async (tagName: string) => {
+ try {
+ await createWsTag({
+ workspaceID: workspaceId,
+ tagName,
+ tagSlug: tagName.replace(' ', '_')
+ });
+ handlePopUpClose('addTag');
+ createNotification({
+ text: 'Successfully created a tag',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to create a tag',
+ type: 'error'
+ });
+ }
+ };
+
+ if (isSecretsLoading || isEnvListLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // when secrets is not loading and secrets list is empty
+ const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
+
+ const numSecretsMissingPerEnv = userAvailableEnvs?.map(envir => ({[envir.slug]: [... new Set(secrets?.secrets.map((secret: any) => secret.key))].length - [... new Set(secrets?.secrets.filter(s => s.env === envir.slug).map((secret: any) => secret.key))].length})).reduce((acc, cur) => ({ ...acc, ...cur }), {})
+
+ return (
+
+
+
+ {
+ handlePopUpToggle('addTag', open);
+ }}
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/DashboardPage.tsx b/frontend/src/views/DashboardPage/DashboardPage.tsx
new file mode 100644
index 000000000..75a42c0df
--- /dev/null
+++ b/frontend/src/views/DashboardPage/DashboardPage.tsx
@@ -0,0 +1,662 @@
+import { useEffect, useState } from 'react';
+import { FormProvider, useFieldArray, useForm, useWatch } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { useRouter } from 'next/router';
+import {
+ faArrowLeft,
+ faCheck,
+ faClockRotateLeft,
+ faCodeCommit,
+ faDownload,
+ faEye,
+ faEyeSlash,
+ faMagnifyingGlass,
+ faPlus
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import { useQueryClient } from '@tanstack/react-query';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+import NavHeader from '@app/components/navigation/NavHeader';
+import {
+ Button,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ TableContainer,
+ Tag,
+ Tooltip
+} from '@app/components/v2';
+import { leaveConfirmDefaultMessage } from '@app/const';
+import { useWorkspace } from '@app/context';
+import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks';
+import {
+ useBatchSecretsOp,
+ useCreateWsTag,
+ useGetProjectSecrets,
+ useGetSecretVersion,
+ useGetSnapshotSecrets,
+ useGetUserAction,
+ useGetUserWsEnvironments,
+ useGetUserWsKey,
+ useGetWorkspaceSecretSnapshots,
+ useGetWsSnapshotCount,
+ useGetWsTags,
+ usePerformSecretRollback,
+ useRegisterUserAction
+} from '@app/hooks/api';
+import { secretKeys } from '@app/hooks/api/secrets/queries';
+import { WorkspaceEnv } from '@app/hooks/api/types';
+
+import { CompareSecret } from './components/CompareSecret';
+import { CreateTagModal } from './components/CreateTagModal';
+import { PitDrawer } from './components/PitDrawer';
+import { SecretDetailDrawer } from './components/SecretDetailDrawer';
+import { SecretDropzone } from './components/SecretDropzone';
+import { SecretInputRow } from './components/SecretInputRow';
+import { SecretTableHeader } from './components/SecretTableHeader';
+import {
+ DEFAULT_SECRET_VALUE,
+ downloadSecret,
+ FormData,
+ schema,
+ transformSecretsToBatchSecretReq,
+ TSecOverwriteOpt,
+ TSecretDetailsOpen
+} from './DashboardPage.utils';
+
+const USER_ACTION_PUSH = 'first_time_secrets_pushed';
+
+/*
+ * Some imp aspects to consider. Here there are multiple stats changing
+ * Thus ideally we need to use a context. But instead we rely on react hook form
+ * React hook form provides context and high performance proxy based rendering
+ * It also handles error handling and transferring states between inputs
+ *
+ * Another thing is the purpose of overrideAction
+ * Before we would remove the value for personal secret when user toggle and user couldn't get it back
+ * They have to reload the browser or go back all over again
+ * Instead when user delete we raise a flag so if user decides to go back to toggle personal before saving
+ * They will get it back
+ */
+export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const { createNotification } = useNotificationContext();
+ const queryClient = useQueryClient();
+
+ const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
+ 'secretDetails',
+ 'addTag',
+ 'secretSnapshots',
+ 'uploadedSecOpts',
+ 'compareSecrets'
+ ] as const);
+ const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
+ const [searchFilter, setSearchFilter] = useState('');
+ const [snapshotId, setSnaphotId] = useState(null);
+ const [selectedEnv, setSelectedEnv] = useState(null);
+ const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
+ const [deletedSecretIds, setDeletedSecretIds] = useState([]);
+ const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
+
+ const { currentWorkspace, isLoading } = useWorkspace();
+ const workspaceId = currentWorkspace?._id as string;
+ const { data: latestFileKey } = useGetUserWsKey(workspaceId);
+
+ useEffect(() => {
+ if (!isLoading && !workspaceId && router.isReady) {
+ router.push('/noprojects');
+ }
+ }, [isLoading, workspaceId, router.isReady]);
+
+ // fetching data
+ const { data: userAction } = useGetUserAction(USER_ACTION_PUSH);
+ const hasUserPushed = Boolean(userAction);
+
+ const { data: wsEnv, isLoading: isEnvListLoading } = useGetUserWsEnvironments({
+ workspaceId,
+ onSuccess: (data) => {
+ // get an env with one of the access available
+ const env = data.find(({ isReadDenied, isWriteDenied }) => !isWriteDenied || !isReadDenied);
+ if (env && data?.map(wsenv => wsenv.slug).includes(envFromTop)) {
+ setSelectedEnv(data?.filter(dp => dp.slug === envFromTop)[0]);
+ }
+ }
+ });
+
+ const { data: secretVersion } = useGetSecretVersion({
+ limit: 10,
+ offset: 0,
+ secretId: (popUp?.secretDetails?.data as TSecretDetailsOpen)?.id,
+ decryptFileKey: latestFileKey!
+ });
+
+ const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
+ workspaceId,
+ env: selectedEnv?.slug || '',
+ decryptFileKey: latestFileKey!,
+ isPaused: Boolean(snapshotId)
+ });
+
+ const {
+ data: secretSnaphots,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage
+ } = useGetWorkspaceSecretSnapshots({
+ workspaceId,
+ limit: 10
+ });
+
+ const {
+ data: snapshotSecret,
+ isLoading: isSnapshotSecretsLoading,
+ isFetching: isSnapshotChanging
+ } = useGetSnapshotSecrets({
+ snapshotId: snapshotId || '',
+ env: selectedEnv?.slug || '',
+ decryptFileKey: latestFileKey!
+ });
+
+ const { data: snapshotCount, isLoading: isLoadingSnapshotCount } =
+ useGetWsSnapshotCount(workspaceId);
+
+ const { data: wsTags } = useGetWsTags(workspaceId);
+ // mutation calls
+ const { mutateAsync: batchSecretOp } = useBatchSecretsOp();
+ const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
+ const { mutateAsync: registerUserAction } = useRegisterUserAction();
+ const { mutateAsync: createWsTag } = useCreateWsTag();
+
+ const method = useForm({
+ // why any: well yup inferred ts expects other keys to defined as undefined
+ defaultValues: secrets as any,
+ values: secrets as any,
+ mode: 'onBlur',
+ resolver: yupResolver(schema)
+ });
+
+ const {
+ control,
+ handleSubmit,
+ getValues,
+ setValue,
+ formState: { isSubmitting, dirtyFields },
+ reset
+ } = method;
+ const formSecrets = useWatch({ control, name: 'secrets' });
+ const { fields, prepend, append, remove, update } = useFieldArray({ control, name: 'secrets' });
+
+ const isRollbackMode = Boolean(snapshotId);
+ const isReadOnly = selectedEnv?.isWriteDenied;
+ const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
+ const canDoRollback = !isReadOnly && !isAddOnly;
+ const isSubmitDisabled =
+ isReadOnly ||
+ // on add only mode the formstate becomes dirty due to secrets missing some items
+ // to avoid this we check dirtyFields in isAddOnly Mode
+ (isAddOnly && Object.keys(dirtyFields).length === 0) ||
+ (!isRollbackMode && !isAddOnly && Object.keys(dirtyFields).length === 0) ||
+ isSubmitting;
+
+ useEffect(() => {
+ if (!isSnapshotChanging && Boolean(snapshotId)) {
+ reset({ secrets: snapshotSecret?.secrets, isSnapshotMode: true });
+ }
+ }, [isSnapshotChanging]);
+
+ useEffect(() => {
+ setHasUnsavedChanges(!isSubmitDisabled);
+ }, [isSubmitDisabled]);
+
+ const onSortSecrets = () => {
+ const dir = sortDir === 'asc' ? 'desc' : 'asc';
+ const sec = getValues('secrets') || [];
+ const sortedSec = sec.sort((a, b) =>
+ dir === 'asc' ? a?.key?.localeCompare(b?.key || '') : b?.key?.localeCompare(a?.key || '')
+ );
+ setValue('secrets', sortedSec);
+ setSortDir(dir);
+ };
+
+ const handleUploadedEnv = (uploadedSec: TSecOverwriteOpt['secrets']) => {
+ const sec = getValues('secrets') || [];
+ const conflictingSec = sec.filter(({ key }) => Boolean(uploadedSec?.[key]));
+ const conflictingSecIds = conflictingSec.reduce>(
+ (prev, curr) => ({
+ ...prev,
+ [curr.key]: true
+ }),
+ {}
+ );
+ // filter to get all conflicting ones
+ const conflictingUploadedSec = { ...uploadedSec };
+ // append non conflicting ones
+ Object.keys(uploadedSec).forEach((key) => {
+ if (!conflictingSecIds?.[key]) {
+ append({
+ ...DEFAULT_SECRET_VALUE,
+ key,
+ value: uploadedSec[key].value,
+ comment: uploadedSec[key].comments.join(',')
+ });
+ delete conflictingUploadedSec[key];
+ }
+ });
+ if (conflictingSec.length > 0) {
+ handlePopUpOpen('uploadedSecOpts', { secrets: conflictingUploadedSec });
+ }
+ };
+
+ const onOverwriteSecrets = () => {
+ const sec = getValues('secrets') || [];
+ const uploadedSec = (popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets;
+ const data: Array<{ key: string; index: number }> = [];
+ sec.forEach(({ key }, index) => {
+ if (uploadedSec?.[key]) data.push({ key, index });
+ });
+ data.forEach(({ key, index }) => {
+ const { value, comments } = uploadedSec[key];
+ const comment = comments.join(', ');
+ update(index, {
+ ...DEFAULT_SECRET_VALUE,
+ key,
+ value,
+ comment,
+ tags: sec[index].tags
+ });
+ });
+ handlePopUpClose('uploadedSecOpts');
+ };
+
+ const onSecretRollback = async () => {
+ if (!snapshotSecret?.version) {
+ createNotification({
+ text: 'Failed to find secret version',
+ type: 'success'
+ });
+ return;
+ }
+ try {
+ await performSecretRollback({
+ workspaceId,
+ version: snapshotSecret.version
+ });
+ setValue('isSnapshotMode', false);
+ setSnaphotId(null);
+ queryClient.invalidateQueries(
+ secretKeys.getProjectSecret(workspaceId, selectedEnv?.slug || '')
+ );
+ createNotification({
+ text: 'Successfully rollback secrets',
+ type: 'success'
+ });
+ } catch (error) {
+ console.log(error);
+ createNotification({
+ text: 'Failed to rollback secrets',
+ type: 'error'
+ });
+ }
+ };
+
+ const onAppendSecret = () => append(DEFAULT_SECRET_VALUE);
+
+ const onSaveSecret = async ({ secrets: userSec = [], isSnapshotMode }: FormData) => {
+ if (isSnapshotMode) {
+ await onSecretRollback();
+ return;
+ }
+ // just closing this if save is triggered from drawer
+ handlePopUpClose('secretDetails');
+ // when add only mode remove rest of things not created
+ const sec = isAddOnly ? userSec.filter(({ _id }) => !_id) : userSec;
+ // encrypt and format the secrets to batch api format
+ // requests = [ {method:"", secret:""} ]
+ const batchedSecret = transformSecretsToBatchSecretReq(deletedSecretIds, latestFileKey, sec);
+ // type check
+ if (!selectedEnv?.slug) return;
+ try {
+ await batchSecretOp({
+ requests: batchedSecret,
+ workspaceId,
+ environment: selectedEnv?.slug
+ });
+ createNotification({
+ text: 'Successfully saved changes',
+ type: 'success'
+ });
+ if (!hasUserPushed) {
+ await registerUserAction(USER_ACTION_PUSH);
+ }
+ } catch (error) {
+ console.log(error);
+ createNotification({
+ text: 'Failed to save changes',
+ type: 'error'
+ });
+ }
+ };
+
+ const onDrawerOpen = (dto: TSecretDetailsOpen) => {
+ handlePopUpOpen('secretDetails', dto);
+ };
+
+ const onEnvChange = (slug: string) => {
+ if (hasUnsavedChanges) {
+ // eslint-disable-next-line no-alert
+ if (!window.confirm(leaveConfirmDefaultMessage)) return;
+ }
+ const env = wsEnv?.find((el) => el.slug === slug);
+ if (env) setSelectedEnv(env);
+ router.push(`${router.asPath.split("?")[0]}?env=${slug}`)
+ };
+
+ // record all deleted ids
+ // This will make final deletion easier
+ const onSecretDelete = (index: number, id?: string, overrideId?: string) => {
+ const ids: string[] = [];
+ if (id) ids.push(id);
+ if (overrideId) ids.push(overrideId);
+ setDeletedSecretIds((state) => [...state, ...ids]);
+ remove(index);
+ // just the case if this is called from drawer
+ handlePopUpClose('secretDetails');
+ };
+
+ const onCreateWsTag = async (tagName: string) => {
+ try {
+ await createWsTag({
+ workspaceID: workspaceId,
+ tagName,
+ tagSlug: tagName.replace(' ', '_')
+ });
+ handlePopUpClose('addTag');
+ createNotification({
+ text: 'Successfully created a tag',
+ type: 'success'
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: 'Failed to create a tag',
+ type: 'error'
+ });
+ }
+ };
+
+ if (isSecretsLoading || isEnvListLoading) {
+ return (
+
+
+
+ );
+ }
+
+ // when secrets is not loading and secrets list is empty
+ const isDashboardSecretEmpty = !isSecretsLoading && !formSecrets?.length;
+ // when using snapshot mode and snapshot is loading and snapshot list is empty
+ const isSnapshotSecretEmtpy =
+ isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
+ const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
+
+ const userAvailableEnvs = wsEnv?.filter(
+ ({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
+ );
+
+ return (
+
+
+
+ {/* Create a new tag modal */}
+ {
+ handlePopUpToggle('addTag', open);
+ }}
+ >
+
+
+
+
+ {/* Uploaded env override or not confirmation modal */}
+ handlePopUpToggle('uploadedSecOpts', open)}
+ >
+ handlePopUpClose('uploadedSecOpts')}
+ >
+ Keep old
+ ,
+
+ Overwrite
+
+ ]}
+ >
+
+
Your file contains following duplicate secrets
+
+ {Object.keys((popUp?.uploadedSecOpts?.data as TSecOverwriteOpt)?.secrets || {})
+ ?.map((key) => key)
+ .join(', ')}
+
+
Are you sure you want to overwrite these secrets?
+
+
+
+ handlePopUpToggle('compareSecrets', open)}
+ >
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/DashboardPage.utils.ts b/frontend/src/views/DashboardPage/DashboardPage.utils.ts
new file mode 100644
index 000000000..ef754d1c5
--- /dev/null
+++ b/frontend/src/views/DashboardPage/DashboardPage.utils.ts
@@ -0,0 +1,231 @@
+import crypto from 'crypto';
+
+import * as yup from 'yup';
+
+import {
+ decryptAssymmetric,
+ encryptSymmetric
+} from '@app/components/utilities/cryptography/crypto';
+import { BatchSecretDTO } from '@app/hooks/api/secrets/types';
+
+export enum SecretActionType {
+ Created = 'created',
+ Modified = 'modified',
+ Deleted = 'deleted'
+}
+
+export const DEFAULT_SECRET_VALUE = {
+ _id: undefined,
+ overrideAction: undefined,
+ idOverride: undefined,
+ valueOverride: undefined,
+ comment: '',
+ key: '',
+ value: '',
+ tags: []
+};
+
+const secretSchema = yup.object({
+ _id: yup.string(),
+ key: yup
+ .string()
+ .trim()
+ .required()
+ .label('Secret key')
+ .test('starts-with-number', 'Should start with an alphabet', (val) =>
+ Boolean(val?.charAt(0)?.match(/[a-zA-Z]/i))
+ )
+ .test({
+ name: 'duplicate-keys',
+ // TODO:(akhilmhdh) ts keeps throwing from not found need to see how to resolve this
+ test: (val, ctx: any) => {
+ const secrets: Array<{ key: string }> = ctx?.from?.[1]?.value?.secrets || [];
+ const duplicateKeys: Record = {};
+ secrets?.forEach(({ key }, index) => {
+ if (key === val) duplicateKeys[index + 1] = true;
+ });
+ const pos = Object.keys(duplicateKeys);
+ if (pos.length <= 1) {
+ return true;
+ }
+ return ctx.createError({ message: `Same key in row ${pos.join(', ')}` });
+ }
+ }),
+ value: yup.string().trim(),
+ comment: yup.string().trim(),
+ tags: yup.array(
+ yup.object({
+ _id: yup.string().required(),
+ name: yup.string().required(),
+ slug: yup.string().required()
+ })
+ ),
+ overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)),
+ idOverride: yup.string().notRequired(),
+ valueOverride: yup.string().trim().notRequired()
+});
+
+export const schema = yup.object({
+ isSnapshotMode: yup.bool().notRequired(),
+ secrets: yup.array(secretSchema)
+});
+
+export type FormData = yup.InferType;
+export type TSecretDetailsOpen = { index: number; id: string };
+export type TSecOverwriteOpt = { secrets: Record };
+
+export const downloadSecret = (secrets: FormData['secrets'] = [], env: string = 'unknown') => {
+ const finalSecret = secrets.map(({ key, value, valueOverride, overrideAction, comment }) => ({
+ key,
+ value: overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value,
+ comment
+ }));
+
+ let file = '';
+ finalSecret.forEach(({ key, value, comment }) => {
+ if (comment) {
+ file += `# ${comment}\n${key}=${value}\n`;
+ return;
+ }
+ file += `${key}=${value}\n`;
+ });
+
+ const blob = new Blob([file]);
+ const fileDownloadUrl = URL.createObjectURL(blob);
+ const alink = document.createElement('a');
+ alink.href = fileDownloadUrl;
+ alink.download = `${env}.env`;
+ alink.click();
+};
+
+/*
+ * Below functions are used convert the dashboard secrets to the bulk secret creation request format
+ * They are encrypted back
+ * Formatted to [ { request: "", secret:{} } ]
+ */
+const encryptASecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
+ // encrypt key
+ const {
+ ciphertext: secretKeyCiphertext,
+ iv: secretKeyIV,
+ tag: secretKeyTag
+ } = encryptSymmetric({
+ plaintext: key,
+ key: randomBytes
+ });
+
+ // encrypt value
+ const {
+ ciphertext: secretValueCiphertext,
+ iv: secretValueIV,
+ tag: secretValueTag
+ } = encryptSymmetric({
+ plaintext: value ?? '',
+ key: randomBytes
+ });
+
+ // encrypt comment
+ const {
+ ciphertext: secretCommentCiphertext,
+ iv: secretCommentIV,
+ tag: secretCommentTag
+ } = encryptSymmetric({
+ plaintext: comment ?? '',
+ key: randomBytes
+ });
+
+ return {
+ secretKeyCiphertext,
+ secretKeyIV,
+ secretKeyTag,
+ secretValueCiphertext,
+ secretValueIV,
+ secretValueTag,
+ secretCommentCiphertext,
+ secretCommentIV,
+ secretCommentTag
+ };
+};
+
+export const transformSecretsToBatchSecretReq = (
+ deletedSecretIds: string[],
+ latestFileKey: any,
+ secrets: FormData['secrets']
+) => {
+ // deleted secrets
+ const secretsToBeDeleted: BatchSecretDTO['requests'] = deletedSecretIds.map((id) => ({
+ method: 'DELETE',
+ secret: { _id: id }
+ }));
+
+ const secretsToBeUpdated: BatchSecretDTO['requests'] = [];
+ const secretsToBeCreated: BatchSecretDTO['requests'] = [];
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+
+ const randomBytes = latestFileKey
+ ? decryptAssymmetric({
+ ciphertext: latestFileKey.encryptedKey,
+ nonce: latestFileKey.nonce,
+ publicKey: latestFileKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ })
+ : crypto.randomBytes(16).toString('hex');
+
+ secrets?.forEach(
+ ({ _id, idOverride, value, valueOverride, overrideAction, tags = [], comment, key }) => {
+ if (!idOverride && overrideAction === SecretActionType.Created) {
+ secretsToBeCreated.push({
+ method: 'POST',
+ secret: {
+ type: 'personal',
+ tags,
+ ...encryptASecret(randomBytes, key, valueOverride, comment)
+ }
+ });
+ }
+ // to be created ones as they don't have server generated id
+ if (!_id) {
+ secretsToBeCreated.push({
+ method: 'POST',
+ secret: {
+ type: 'shared',
+ tags,
+ ...encryptASecret(randomBytes, key, value, comment)
+ }
+ });
+ return; // exit as updated and delete case won't happen when created
+ }
+ // has an id means this is updated one
+ if (_id) {
+ secretsToBeUpdated.push({
+ method: 'PATCH',
+ secret: {
+ _id,
+ type: 'shared',
+ tags,
+ ...encryptASecret(randomBytes, key, value, comment)
+ }
+ });
+ }
+ if (idOverride) {
+ // if action is deleted meaning override has been removed but id is kept to collect at this point
+ if (overrideAction === SecretActionType.Deleted) {
+ secretsToBeDeleted.push({ method: 'DELETE', secret: { _id: idOverride } });
+ } else {
+ // if not deleted action then as id is there its an updated
+ secretsToBeUpdated.push({
+ method: 'PATCH',
+ secret: {
+ _id: idOverride,
+ type: 'personal',
+ tags,
+ ...encryptASecret(randomBytes, key, valueOverride, comment)
+ }
+ });
+ }
+ }
+ }
+ );
+
+ return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted);
+};
diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx
new file mode 100644
index 000000000..9075595aa
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/CompareSecret/CompareSecret.tsx
@@ -0,0 +1,64 @@
+import { useCallback } from 'react';
+
+import { FormControl, Input, Spinner } from '@app/components/v2';
+import { useGetProjectSecrets, useGetUserWsKey } from '@app/hooks/api';
+
+type SecretValueProps = {
+ workspaceId: string;
+ envName: string;
+ env: string;
+ secretKey: string;
+};
+
+const SecretValue = ({ workspaceId, env, envName, secretKey }: SecretValueProps) => {
+ const { data: latestFileKey } = useGetUserWsKey(workspaceId);
+ const { data: secret, isLoading: isSecretsLoading } = useGetProjectSecrets({
+ workspaceId,
+ env,
+ decryptFileKey: latestFileKey!
+ });
+
+ const getValue = useCallback(
+ (data: typeof secret) => {
+ const sec = data?.secrets?.find(({ key: secKey }) => secKey === secretKey);
+ return sec?.value || 'Not found';
+ },
+ [secretKey]
+ );
+
+ return (
+
+ : undefined}
+ />
+
+ );
+};
+
+type Props = {
+ workspaceId: string;
+ secretKey: string;
+ envs: Array<{ name: string; slug: string }>;
+};
+
+export const CompareSecret = ({ workspaceId, secretKey, envs }: Props): JSX.Element => {
+ // should not do anything until secretKey is available
+ if (!secretKey) return
;
+
+ return (
+
+ {envs.map(({ name, slug }) => (
+
+ ))}
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx b/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx
new file mode 100644
index 000000000..871a6529d
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/CompareSecret/index.tsx
@@ -0,0 +1 @@
+export { CompareSecret } from './CompareSecret';
diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx
new file mode 100644
index 000000000..541417cbc
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/CreateTagModal/CreateTagModal.tsx
@@ -0,0 +1,55 @@
+import { Controller, useForm } from 'react-hook-form';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import { Button, FormControl, Input, ModalClose } from '@app/components/v2';
+
+type Props = {
+ onCreateTag: (tagName: string) => Promise;
+};
+
+const createTagSchema = yup.object({
+ name: yup.string().required().trim().label('Tag Name')
+});
+type FormData = yup.InferType;
+
+export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => {
+ const {
+ control,
+ reset,
+ formState: { isSubmitting },
+ handleSubmit
+ } = useForm({
+ resolver: yupResolver(createTagSchema)
+ });
+
+ const onFormSubmit = async ({ name }: FormData) => {
+ await onCreateTag(name);
+ reset();
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx b/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx
new file mode 100644
index 000000000..a660c64fd
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/CreateTagModal/index.tsx
@@ -0,0 +1 @@
+export {CreateTagModal} from './CreateTagModal'
\ No newline at end of file
diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx
new file mode 100644
index 000000000..acadf600d
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/EnvComparisonRow.tsx
@@ -0,0 +1,131 @@
+/* eslint-disable react/jsx-no-useless-fragment */
+import { SyntheticEvent, useRef, useState } from 'react';
+import { useFormContext, useWatch } from 'react-hook-form';
+import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import guidGenerator from '@app/components/utilities/randomId';
+
+import { FormData } from '../../DashboardPage.utils';
+
+type Props = {
+ index: number;
+ secrets: any[] | undefined;
+ // permission and external state's that decided to hide or show
+ isReadOnly?: boolean;
+ isSecretValueHidden: boolean;
+ userAvailableEnvs?: any[];
+};
+
+const REGEX = /([$]{.*?})/g;
+
+const DashboardInput = ({ isOverridden, isSecretValueHidden, isReadOnly, secret, index }: { isOverridden: boolean, isSecretValueHidden: boolean, isReadOnly?: boolean, secret: any, index: number } ): JSX.Element => {
+ const ref = useRef(null);
+ const syncScroll = (e: SyntheticEvent) => {
+ if (ref.current === null) return;
+
+ ref.current.scrollTop = e.currentTarget.scrollTop;
+ ref.current.scrollLeft = e.currentTarget.scrollLeft;
+ };
+
+ return
+
+
+
+ {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split('').length === 0 && EMPTY }
+ {(secret?.value || secret?.value === '') && (isOverridden ? secret.valueOverride : secret?.value)?.split(REGEX).map((word: string) => {
+ if (word.match(REGEX) !== null) {
+ return (
+
+ {word.slice(0, 2)}
+
+ {word.slice(2, word.length - 1)}
+
+ {word.slice(word.length - 1, word.length) === '}' ? (
+
+ {word.slice(word.length - 1, word.length)}
+
+ ) : (
+
+ {word.slice(word.length - 1, word.length)}
+
+ )}
+
+ );
+ }
+ return (
+
+ {word}
+
+ );
+ })}
+ {!(secret?.value || secret?.value === '') && missing }
+
+ {(isSecretValueHidden && secret?.value) && (
+
+
+ {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').map(() => (
+
+ ))}
+ {(isOverridden ? secret.valueOverride : secret?.value || '')?.split('').length === 0 && EMPTY }
+
+
+ )}
+
+
+}
+
+export const EnvComparisonRow = ({
+ index,
+ secrets,
+ isSecretValueHidden,
+ isReadOnly,
+ userAvailableEnvs
+}: Props): JSX.Element => {
+ const {
+ // register, setValue,
+ control } = useFormContext();
+
+ // to get details on a secret
+ const secret = useWatch({ name: `secrets.${index}`, control });
+
+ const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
+
+ return (
+
+ {index + 1}
+
+ {secret?.key || ''}
+ setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}>
+
+
+
+ {userAvailableEnvs?.map(env => {
+ return sec.env === env.slug)[0]} index={index} />
+ })}
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx
new file mode 100644
index 000000000..e0c9f8847
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/EnvComparisonRow/index.tsx
@@ -0,0 +1 @@
+export { EnvComparisonRow } from './EnvComparisonRow';
diff --git a/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx b/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx
new file mode 100644
index 000000000..a398530b2
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/PitDrawer/PitDrawer.tsx
@@ -0,0 +1,83 @@
+import { Fragment, useCallback } from 'react';
+import { InfiniteData } from '@tanstack/react-query';
+
+import { Button, Drawer, DrawerContent } from '@app/components/v2';
+import timeSince from '@app/ee/utilities/timeSince';
+import { TWorkspaceSecretSnapshot } from '@app/hooks/api/secretSnapshots/types';
+
+type Props = {
+ isDrawerOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ secretSnaphots?: InfiniteData;
+ onSelectSnapshot: (id: string) => void;
+ snapshotId: string | null;
+ isFetchingNextPage?: boolean;
+ hasNextPage?: boolean;
+ fetchNextPage: () => void;
+};
+
+export const PitDrawer = ({
+ isDrawerOpen,
+ onOpenChange,
+ secretSnaphots,
+ onSelectSnapshot,
+ snapshotId,
+ isFetchingNextPage,
+ hasNextPage,
+ fetchNextPage
+}: Props): JSX.Element => {
+ const getButtonLabel = useCallback((isFirstChild: boolean, isSelectedSnapshot: boolean) => {
+ if (isFirstChild) return 'Current Version';
+ if (isSelectedSnapshot) return 'Currently Viewing';
+ return 'Explore';
+ }, []);
+
+ return (
+
+
+
+ {secretSnaphots?.pages?.map((group, i) => (
+
+ {group.map(({ _id, createdAt }, index) => (
+ onSelectSnapshot(_id)}
+ >
+
+
{timeSince(new Date(createdAt))}
+
{getButtonLabel(i === 0 && index === 0, snapshotId === _id)}
+
+
+ ))}
+
+ ))}
+
+
+ {hasNextPage ? 'Load More' : 'End of history'}
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx b/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx
new file mode 100644
index 000000000..993b641ff
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/PitDrawer/index.tsx
@@ -0,0 +1 @@
+export { PitDrawer } from './PitDrawer';
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx
new file mode 100644
index 000000000..d64a6ad36
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/GenRandomNumber.tsx
@@ -0,0 +1,52 @@
+import crypto from 'crypto';
+
+import { useState } from 'react';
+import { useTranslation } from 'next-i18next';
+import { faMinus, faPlus, faShuffle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { Button, Input } from '@app/components/v2';
+
+type Props = {
+ onGenerate: (val: string) => void;
+};
+
+export const GenRandomNumber = ({ onGenerate }: Props) => {
+ const { t } = useTranslation();
+ const [value, setValue] = useState(32);
+
+ const onGenerateRandomHex = () => {
+ const rand = crypto.randomBytes(value).toString('hex');
+ onGenerate(rand);
+ };
+
+ return (
+
+
}
+ onClick={onGenerateRandomHex}
+ >
+ {t('dashboard:sidebar.generate-random-hex')}
+
+
+
+ setValue((val) => val - 1)}>
+
+
+ setValue(parseInt(e.target.value, 10))}
+ />
+ setValue((val) => val + 1)}>
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx
new file mode 100644
index 000000000..f0d64819f
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/SecretDetailDrawer.tsx
@@ -0,0 +1,211 @@
+import { useFormContext } from 'react-hook-form';
+import { faCircle, faCircleDot, faShuffle } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import {
+ Button,
+ Drawer,
+ DrawerContent,
+ FormControl,
+ Input,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ Switch,
+ TextArea
+} from '@app/components/v2';
+import { useToggle } from '@app/hooks';
+
+import { FormData, SecretActionType } from '../../DashboardPage.utils';
+import { GenRandomNumber } from './GenRandomNumber';
+
+type Props = {
+ isDrawerOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ index: number;
+ isReadOnly?: boolean;
+ onEnvCompare: (secretKey: string) => void;
+ secretVersion?: Array<{ id: string; createdAt: string; value: string }>;
+ // to record the ids of deleted ones
+ onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
+ onSave: () => void;
+};
+
+export const SecretDetailDrawer = ({
+ isDrawerOpen,
+ onOpenChange,
+ index,
+ secretVersion = [],
+ isReadOnly,
+ onSecretDelete,
+ onSave,
+ onEnvCompare
+}: Props): JSX.Element => {
+ const [canRevealSecVal, setCanRevealSecVal] = useToggle();
+ const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
+
+ const { register, setValue, watch } = useFormContext();
+ const secret = watch(`secrets.${index}`);
+
+ const isOverridden =
+ secret?.overrideAction === SecretActionType.Created ||
+ secret?.overrideAction === SecretActionType.Modified;
+
+ const onSecretOverride = () => {
+ if (isOverridden) {
+ // when user created a new override but then removes
+ if (SecretActionType.Created) {
+ setValue(`secrets.${index}.valueOverride`, '', { shouldDirty: true });
+ }
+ setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true });
+ } else {
+ setValue(
+ `secrets.${index}.overrideAction`,
+ secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created,
+ { shouldDirty: true }
+ );
+ }
+ };
+
+ if (!secret) {
+ return
;
+ }
+
+ return (
+
+
+
+ onEnvCompare(secret?.key)}
+ isFullWidth
+ isDisabled={isReadOnly}
+ >
+ Compare secret across environments
+
+
+
+
+ Save Changes
+
+ onSecretDelete(index, secret._id, secret.idOverride)}
+ >
+ Delete
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ setValue(`secrets.${index}.value`, val, { shouldDirty: true })
+ }
+ />
+
+
+
+
+
+ Override with a personal value
+
+
+
+
+
+
+
+ }
+ />
+
+
+ setValue(`secrets.${index}.valueOverride`, val, { shouldDirty: true })
+ }
+ />
+
+
+
+
+
Version History
+
+ {secretVersion?.map(({ createdAt, value, id }, i) => (
+
+
+
+
+
+
+ {new Date(createdAt).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit'
+ })}
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/SecretDetailDrawer/index.tsx b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/index.tsx
new file mode 100644
index 000000000..3b93bffd6
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretDetailDrawer/index.tsx
@@ -0,0 +1 @@
+export {SecretDetailDrawer} from './SecretDetailDrawer'
\ No newline at end of file
diff --git a/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx
new file mode 100644
index 000000000..e3a871819
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretDropzone/SecretDropzone.tsx
@@ -0,0 +1,130 @@
+import { ChangeEvent, DragEvent } from 'react';
+import { useTranslation } from 'next-i18next';
+import { faUpload } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { twMerge } from 'tailwind-merge';
+
+import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider';
+// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionalityj
+import { parseDotEnv } from '@app/components/utilities/parseDotEnv';
+import { Button } from '@app/components/v2';
+import { useToggle } from '@app/hooks/useToggle';
+
+type Props = {
+ isSmaller: boolean;
+ onParsedEnv: (env: Record) => void;
+ onAddNewSecret?: () => void;
+};
+
+export const SecretDropzone = ({ isSmaller, onParsedEnv, onAddNewSecret }: Props): JSX.Element => {
+ const { t } = useTranslation();
+ const [isDragActive, setDragActive] = useToggle();
+ const [isLoading, setIsLoading] = useToggle();
+ const { createNotification } = useNotificationContext();
+
+ const handleDrag = (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.type === 'dragenter' || e.type === 'dragover') {
+ setDragActive.on();
+ } else if (e.type === 'dragleave') {
+ setDragActive.off();
+ }
+ };
+
+ const parseFile = (file?: File) => {
+ const reader = new FileReader();
+ if (!file) {
+ createNotification({
+ text: `You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.`,
+ type: 'error',
+ timeoutMs: 10000
+ });
+ return;
+ }
+ // const fileType = file.name.split('.')[1];
+ setIsLoading.on();
+ reader.onload = (event) => {
+ if (!event?.target?.result) return;
+ // parse function's argument looks like to be ArrayBuffer
+ const env = parseDotEnv(event.target.result as ArrayBuffer);
+ setIsLoading.off();
+ onParsedEnv(env);
+ };
+
+ // If something is wrong show an error
+ try {
+ reader.readAsText(file);
+ } catch (error) {
+ console.log(error);
+ }
+ };
+
+ const handleDrop = (e: DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!e.dataTransfer) {
+ return;
+ }
+
+ e.dataTransfer.dropEffect = 'copy';
+ setDragActive.off();
+ parseFile(e.dataTransfer.files[0]);
+ };
+
+ const handleFileUpload = (e: ChangeEvent) => {
+ e.preventDefault();
+ parseFile(e.target?.files?.[0]);
+ };
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+
+
+
+
+
{t(isSmaller ? 'common:drop-zone-keys' : 'common:drop-zone')}
+
+
+ {!isSmaller && (
+ <>
+
+
+
+ Add a new secret
+
+
+ >
+ )}{' '}
+ >
+ )}
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/SecretDropzone/index.tsx b/frontend/src/views/DashboardPage/components/SecretDropzone/index.tsx
new file mode 100644
index 000000000..dc1752390
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretDropzone/index.tsx
@@ -0,0 +1 @@
+export { SecretDropzone } from './SecretDropzone';
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
new file mode 100644
index 000000000..f0b2cd092
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretInputRow/SecretInputRow.tsx
@@ -0,0 +1,424 @@
+/* eslint-disable react/jsx-no-useless-fragment */
+import { SyntheticEvent, useRef } from 'react';
+import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
+import {
+ faCircle,
+ faCodeBranch,
+ faComment,
+ faEllipsis,
+ faInfoCircle,
+ faPlus,
+ faTags,
+ faXmark
+} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { cx } from 'cva';
+import { twMerge } from 'tailwind-merge';
+
+import guidGenerator from '@app/components/utilities/randomId';
+import {
+ Button,
+ Checkbox,
+ FormControl,
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+ IconButton,
+ Input,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+ Tag,
+ TextArea,
+ Tooltip
+} from '@app/components/v2';
+import { useToggle } from '@app/hooks';
+import { WsTag } from '@app/hooks/api/types';
+
+import { FormData, SecretActionType } from '../../DashboardPage.utils';
+
+type Props = {
+ index: number;
+ // permission and external state's that decided to hide or show
+ isReadOnly?: boolean;
+ isAddOnly?: boolean;
+ isRollbackMode?: boolean;
+ isSecretValueHidden: boolean;
+ searchTerm: string;
+ // to record the ids of deleted ones
+ onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
+ // sidebar control props
+ onRowExpand: () => void;
+ // tag props
+ wsTags?: WsTag[];
+ onCreateTagOpen: () => void;
+};
+
+const tagColors = [
+ { bg: 'bg-[#f1c40f]/40', text: 'text-[#fcf0c3]/70' },
+ { bg: 'bg-[#cb1c8d]/40', text: 'text-[#f2c6e3]/70' },
+ { bg: 'bg-[#badc58]/40', text: 'text-[#eef6d5]/70' },
+ { bg: 'bg-[#ff5400]/40', text: 'text-[#ffddcc]/70' },
+ { bg: 'bg-[#3AB0FF]/40', text: 'text-[#f0fffd]/70' },
+ { bg: 'bg-[#6F1AB6]/40', text: 'text-[#FFE5F1]/70' },
+ { bg: 'bg-[#C40B13]/40', text: 'text-[#FFDEDE]/70' },
+ { bg: 'bg-[#332FD0]/40', text: 'text-[#DFF6FF]/70' }
+];
+const REGEX = /([$]{.*?})/g;
+
+export const SecretInputRow = ({
+ index,
+ isSecretValueHidden,
+ onRowExpand,
+ isReadOnly,
+ isRollbackMode,
+ isAddOnly,
+ wsTags,
+ onCreateTagOpen,
+ onSecretDelete,
+ searchTerm
+}: Props): JSX.Element => {
+ const ref = useRef(null);
+ const syncScroll = (e: SyntheticEvent) => {
+ if (ref.current === null) return;
+
+ ref.current.scrollTop = e.currentTarget.scrollTop;
+ ref.current.scrollLeft = e.currentTarget.scrollLeft;
+ };
+ const { register, setValue, control } = useFormContext();
+ const [canRevealSecret] = useToggle();
+ // comment management in a row
+ const {
+ fields: secretTags,
+ remove,
+ append
+ } = useFieldArray({ control, name: `secrets.${index}.tags` });
+
+ // to get details on a secret
+ const secret = useWatch({ name: `secrets.${index}`, control });
+ const hasComment = Boolean(secret.comment);
+ const tags = secret.tags || [];
+ const selectedTagIds = tags.reduce>(
+ (prev, curr) => ({ ...prev, [curr.slug]: true }),
+ {}
+ );
+
+ // when secret is override by personal values
+ const isOverridden =
+ secret.overrideAction === SecretActionType.Created ||
+ secret.overrideAction === SecretActionType.Modified;
+
+ const onSecretOverride = () => {
+ if (isOverridden) {
+ // when user created a new override but then removes
+ if (secret?.overrideAction === SecretActionType.Created)
+ setValue(`secrets.${index}.valueOverride`, '');
+ setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true });
+ } else {
+ setValue(`secrets.${index}.valueOverride`, '');
+ setValue(
+ `secrets.${index}.overrideAction`,
+ secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created,
+ { shouldDirty: true }
+ );
+ }
+ };
+
+ const onSelectTag = (selectedTag: WsTag) => {
+ const shouldAppend = !selectedTagIds[selectedTag.slug];
+ if (shouldAppend) {
+ append(selectedTag);
+ } else {
+ const pos = tags.findIndex(({ slug }) => selectedTag.slug === slug);
+ remove(pos);
+ }
+ };
+
+ const isCreatedSecret = !secret?._id;
+ const shouldBeBlockedInAddOnly = !isCreatedSecret && isAddOnly;
+
+ // Why this instead of filter in parent
+ // Because rhf field.map has default values so basically
+ // keys are not updated there and index needs to kept so that we can monitor
+ // values individually here
+ if (
+ !(
+ secret.key?.toUpperCase().includes(searchTerm?.toUpperCase()) ||
+ tags
+ ?.map((tag) => tag.name)
+ .join(' ')
+ ?.toUpperCase()
+ .includes(searchTerm?.toUpperCase()) ||
+ secret.comment?.toUpperCase().includes(searchTerm?.toUpperCase())
+ )
+ ) {
+ return <>>;
+ }
+
+ return (
+
+ {index + 1}
+ (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isAddOnly && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
{error?.message}
+
+
+
+ )}
+ />
+
+
+ {isOverridden
+ ?
+ :
}
+
+ {(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && EMPTY }
+ {(isOverridden ? secret.valueOverride : secret.value)?.split(REGEX).map((word) => {
+ if (word.match(REGEX) !== null) {
+ return (
+
+ {word.slice(0, 2)}
+
+ {word.slice(2, word.length - 1)}
+
+ {word.slice(word.length - 1, word.length) === '}' ? (
+
+ {word.slice(word.length - 1, word.length)}
+
+ ) : (
+
+ {word.slice(word.length - 1, word.length)}
+
+ )}
+
+ );
+ }
+ return (
+
+ {word}
+
+ );
+ })}
+
+ {(!canRevealSecret && isSecretValueHidden) && (
+
+
+ {(isOverridden ? secret.valueOverride : secret.value)?.split('').map(() => (
+
+ ))}
+ {(isOverridden ? secret.valueOverride : secret.value)?.split('').length === 0 && EMPTY }
+
+
+ )}
+
+
+
+
+ {secretTags.map(({ id, slug }, i) => (
+
remove(i)}
+ key={id}
+ >
+ {slug}
+
+ ))}
+ {!(isReadOnly || isAddOnly || isRollbackMode) && (
+
+
+
+
+
+
+
+
+
+
+
+
+ Add tags to {secret.key || "this secret"}
+
+ {wsTags?.map((wsTag) => (
+ onSelectTag(wsTag)}
+ leftIcon={
+ {}}
+ >
+ {}
+
+ }
+ key={wsTag._id}
+ >
+ {wsTag.slug}
+
+ ))}
+ }
+ >
+ Add new tag
+
+
+
+
+
+ )}
+
+
+ {!isAddOnly && (
+
+
+
+
+
+
+
+ )}
+
+
+ onSecretDelete(index, secret._id, secret?.idOverride)}
+ >
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/src/views/DashboardPage/components/SecretInputRow/index.tsx b/frontend/src/views/DashboardPage/components/SecretInputRow/index.tsx
new file mode 100644
index 000000000..94701da38
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretInputRow/index.tsx
@@ -0,0 +1 @@
+export { SecretInputRow } from './SecretInputRow';
diff --git a/frontend/src/views/DashboardPage/components/SecretTableHeader/SecretTableHeader.tsx b/frontend/src/views/DashboardPage/components/SecretTableHeader/SecretTableHeader.tsx
new file mode 100644
index 000000000..2c7631298
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretTableHeader/SecretTableHeader.tsx
@@ -0,0 +1,44 @@
+import { Controller } from 'react-hook-form';
+import { faArrowDown, faArrowUp } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { IconButton } from '@app/components/v2';
+
+type Props = {
+ sortDir: 'asc' | 'desc';
+ onSort: () => void;
+};
+
+export const SecretTableHeader = ({
+ sortDir,
+ onSort
+}: Props): JSX.Element => (
+
+
+
+ {0}
+
+ (
+
+
+
+ )}
+ />
+ Value
+
+
+
+);
diff --git a/frontend/src/views/DashboardPage/components/SecretTableHeader/index.tsx b/frontend/src/views/DashboardPage/components/SecretTableHeader/index.tsx
new file mode 100644
index 000000000..ad0332905
--- /dev/null
+++ b/frontend/src/views/DashboardPage/components/SecretTableHeader/index.tsx
@@ -0,0 +1 @@
+export { SecretTableHeader } from './SecretTableHeader';
diff --git a/frontend/src/views/DashboardPage/index.tsx b/frontend/src/views/DashboardPage/index.tsx
new file mode 100644
index 000000000..d1e7bf228
--- /dev/null
+++ b/frontend/src/views/DashboardPage/index.tsx
@@ -0,0 +1 @@
+export { DashboardPage } from './DashboardPage';
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx
new file mode 100644
index 000000000..56914f7df
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/CreateServiceAccountPage.tsx
@@ -0,0 +1,46 @@
+import { useRouter } from 'next/router';
+
+import NavHeader from '@app/components/navigation/NavHeader';
+
+import { SAProjectLevelPermissionsTable } from './components/SAProjectLevelPermissionsTable';
+import {
+ CopyServiceAccountPublicKeySection,
+ ServiceAccountNameChangeSection
+} from './components';
+
+export const CreateServiceAccountPage = () => {
+ const router = useRouter();
+ const {serviceAccountId} = router.query;
+
+ return (
+
+
+
+
Service Account
+
+ A service account represents a machine identity such as a VM or application client.
+
+
+ {typeof serviceAccountId === 'string' && (
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx
new file mode 100644
index 000000000..768145b09
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/CopyServiceAccountIDSection.tsx
@@ -0,0 +1,49 @@
+import { useEffect } from 'react';
+import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { IconButton } from '@app/components/v2';
+import { useToggle } from '@app/hooks';
+
+type Props = {
+ serviceAccountId: string;
+}
+
+export const CopyServiceAccountIDSection = ({ serviceAccountId }: Props): JSX.Element => {
+ const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+
+ if (isServiceAccountIdCopied) {
+ timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000);
+ }
+
+ return () => clearTimeout(timer);
+ }, [isServiceAccountIdCopied]);
+
+ const copyServiceAccountIdToClipboard = () => {
+ navigator.clipboard.writeText(serviceAccountId);
+ setIsServiceAccountIdCopied.on();
+ };
+
+ return (
+
+
Service Account ID
+
+
{serviceAccountId}
+
copyServiceAccountIdToClipboard()}
+ >
+
+
+ Copy
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx
new file mode 100644
index 000000000..01cae04d6
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountIDSection/index.tsx
@@ -0,0 +1 @@
+export { CopyServiceAccountIDSection } from './CopyServiceAccountIDSection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx
new file mode 100644
index 000000000..e12001985
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/CopyServiceAccountPublicKeySection.tsx
@@ -0,0 +1,53 @@
+import { useEffect } from 'react';
+import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+import { IconButton } from '@app/components/v2';
+import { useToggle } from '@app/hooks';
+import { useGetServiceAccountById } from '@app/hooks/api';
+
+type Props = {
+ serviceAccountId: string;
+}
+
+export const CopyServiceAccountPublicKeySection = ({ serviceAccountId }: Props): JSX.Element => {
+ const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
+ const [isServiceAccountIdCopied, setIsServiceAccountIdCopied] = useToggle(false);
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+
+ if (isServiceAccountIdCopied) {
+ timer = setTimeout(() => setIsServiceAccountIdCopied.off(), 2000);
+ }
+
+ return () => clearTimeout(timer);
+ }, [isServiceAccountIdCopied]);
+
+ const copyServiceAccountIdToClipboard = () => {
+ if (!serviceAccount) return;
+
+ navigator.clipboard.writeText(serviceAccount.publicKey);
+ setIsServiceAccountIdCopied.on();
+ };
+
+ return serviceAccount ? (
+
+
Public Key
+
+
{serviceAccount.publicKey}
+
copyServiceAccountIdToClipboard()}
+ >
+
+
+ Copy
+
+
+
+
+ ) :
+}
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx
new file mode 100644
index 000000000..d9ed7f56f
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/CopyServiceAccountPublicKeySection/index.tsx
@@ -0,0 +1 @@
+export { CopyServiceAccountPublicKeySection } from './CopyServiceAccountPublicKeySection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx
new file mode 100644
index 000000000..170e71e7e
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/SAProjectLevelPermissionsTable.tsx
@@ -0,0 +1,407 @@
+import { useState } from 'react';
+import { Controller,useForm } from 'react-hook-form';
+import {
+ faKey,
+ faMagnifyingGlass,
+ faPlus,
+ faTrash} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ decryptAssymmetric,
+ encryptAssymmetric,
+ verifyPrivateKey} from '@app/components/utilities/cryptography/crypto';
+import {
+ Button,
+ Checkbox,
+ DeleteActionModal,
+ EmptyState,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalClose,
+ ModalContent,
+ Select,
+ SelectItem,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr} from '@app/components/v2';
+import { usePopUp } from '@app/hooks';
+import {
+ useCreateServiceAccountProjectLevelPermission,
+ useDeleteServiceAccountProjectLevelPermission,
+ useGetServiceAccountById,
+ useGetServiceAccountProjectLevelPermissions,
+ useGetUserWorkspaces
+} from '@app/hooks/api';
+import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey';
+
+const createProjectLevelPermissionSchema = yup.object({
+ privateKey: yup.string().required().label('Private Key'),
+ workspace: yup.string().required().label('Workspace'),
+ environment: yup.string().required().label('Environment'),
+ permissions: yup.object().shape({
+ read: yup.boolean().required(),
+ write: yup.boolean().required()
+ }).defined().required()
+});
+
+type CreateProjectLevelPermissionForm = yup.InferType;
+
+type Props = {
+ serviceAccountId: string;
+}
+
+export const SAProjectLevelPermissionsTable = ({
+ serviceAccountId
+}: Props): JSX.Element => {
+ const { data: serviceAccount } = useGetServiceAccountById(serviceAccountId);
+ const { data: userWorkspaces, isLoading: isUserWorkspacesLoading } = useGetUserWorkspaces();
+ const [searchPermissions, setSearchPermissions] = useState('');
+
+ const { data: serviceAccountWorkspacePermissions, isLoading: isPermissionsLoading } = useGetServiceAccountProjectLevelPermissions(serviceAccountId);
+
+ const createServiceAccountProjectLevelPermission = useCreateServiceAccountProjectLevelPermission();
+ const deleteServiceAccountProjectLevelPermission = useDeleteServiceAccountProjectLevelPermission();
+
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addProjectLevelPermission',
+ 'removeProjectLevelPermission',
+ ] as const);
+
+ const [, setSelectedWorkspace] = useState(undefined);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(createProjectLevelPermissionSchema) })
+
+ const onAddProjectLevelPermission = async ({
+ privateKey,
+ workspace,
+ environment,
+ permissions: { read, write }
+ }: CreateProjectLevelPermissionForm) => {
+
+ // TODO: clean up / modularize this function
+
+ if (!serviceAccount) return;
+
+ const { latestKey } = await getLatestFileKey({
+ workspaceId: workspace
+ });
+
+ verifyPrivateKey({
+ privateKey,
+ publicKey: serviceAccount.publicKey
+ });
+
+ const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
+
+ const key = decryptAssymmetric({
+ ciphertext: latestKey.encryptedKey,
+ nonce: latestKey.nonce,
+ publicKey: latestKey.sender.publicKey,
+ privateKey: PRIVATE_KEY
+ });
+
+ const { ciphertext, nonce } = encryptAssymmetric({
+ plaintext: key,
+ publicKey: serviceAccount.publicKey,
+ privateKey
+ });
+
+ await createServiceAccountProjectLevelPermission.mutateAsync({
+ serviceAccountId,
+ workspaceId: workspace,
+ environment,
+ read,
+ write,
+ encryptedKey: ciphertext,
+ nonce
+ });
+ handlePopUpClose('addProjectLevelPermission');
+ }
+
+ const onRemoveProjectLevelPermission = async () => {
+ const serviceAccountWorkspacePermissionId = (popUp?.removeProjectLevelPermission?.data as { _id: string })?._id;
+ await deleteServiceAccountProjectLevelPermission.mutateAsync({
+ serviceAccountId,
+ serviceAccountWorkspacePermissionId
+ });
+ handlePopUpClose('removeProjectLevelPermission');
+ }
+
+ return (
+
+
Project-Level Permissions
+
+
+ setSearchPermissions(e.target.value)}
+ leftIcon={ }
+ placeholder="Search service account project-level permissions..."
+ />
+
+
}
+ onClick={() => {
+ handlePopUpOpen('addProjectLevelPermission')
+ reset();
+ }}
+ >
+ Add Permission
+
+
+
+
+
+
+ Project
+ Environment
+ Read
+ Write
+
+
+
+
+ {isPermissionsLoading && }
+ {!isPermissionsLoading && serviceAccountWorkspacePermissions && (
+ serviceAccountWorkspacePermissions.map(({
+ _id,
+ workspace,
+ environment,
+ read,
+ write
+ }) => {
+ const environmentName = (workspace.environments.find((env) => env.slug === environment))?.name;
+ return (
+
+ {workspace.name}
+ {environmentName}
+
+ {/**/}
+
+
+ {/**/}
+
+
+ handlePopUpOpen('removeProjectLevelPermission', { _id })}
+ >
+
+
+
+
+ );
+ })
+ )}
+ {!isPermissionsLoading && serviceAccountWorkspacePermissions?.length === 0 && (
+
+
+
+
+
+ )}
+
+
+
+
{
+ handlePopUpToggle('addProjectLevelPermission', isOpen);
+ }}
+ >
+
+
+
+
+
handlePopUpToggle('removeProjectLevelPermission', isOpen)}
+ onDeleteApproved={onRemoveProjectLevelPermission}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx
new file mode 100644
index 000000000..164b60d51
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/SAProjectLevelPermissionsTable/index.tsx
@@ -0,0 +1 @@
+export { SAProjectLevelPermissionsTable } from './SAProjectLevelPermissionsTable';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx
new file mode 100644
index 000000000..7bb7d3414
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/ServiceAccountNameChangeSection.tsx
@@ -0,0 +1,88 @@
+import { useEffect } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import { faCheck } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import {
+ Button,
+ FormControl,
+ Input} from '@app/components/v2';
+import {
+ useGetServiceAccountById,
+ useRenameServiceAccount
+} from '@app/hooks/api';
+
+const formSchema = yup.object({
+ name: yup.string().required().label('Service Account Name')
+});
+
+type FormData = yup.InferType;
+
+type Props = {
+ serviceAccountId: string;
+}
+
+export const ServiceAccountNameChangeSection = ({
+ serviceAccountId
+}: Props) => {
+ const { data: serviceAccount, isLoading: isServiceAccountLoading } = useGetServiceAccountById(serviceAccountId);
+
+ const renameServiceAccount = useRenameServiceAccount();
+
+ const {
+ handleSubmit,
+ control,
+ reset,
+ formState: { isDirty, isSubmitting }
+ } = useForm({ resolver: yupResolver(formSchema) });
+
+ useEffect(() => {
+ reset({ name: serviceAccount?.name });
+ }, [serviceAccount?.name]);
+
+ const onFormSubmit = async ({ name }: FormData) => {
+ try {
+ await renameServiceAccount.mutateAsync({
+ serviceAccountId,
+ name
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx
new file mode 100644
index 000000000..711dae779
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/ServiceAccountNameChangeSection/index.tsx
@@ -0,0 +1 @@
+export { ServiceAccountNameChangeSection } from './ServiceAccountNameChangeSection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx
new file mode 100644
index 000000000..141981735
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/components/index.tsx
@@ -0,0 +1,4 @@
+export { CopyServiceAccountIDSection } from './CopyServiceAccountIDSection';
+export { CopyServiceAccountPublicKeySection } from './CopyServiceAccountPublicKeySection';
+export { SAProjectLevelPermissionsTable } from './SAProjectLevelPermissionsTable';
+export { ServiceAccountNameChangeSection } from './ServiceAccountNameChangeSection';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx b/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx
new file mode 100644
index 000000000..b308a6841
--- /dev/null
+++ b/frontend/src/views/Settings/CreateServiceAccountPage/index.tsx
@@ -0,0 +1 @@
+export { CreateServiceAccountPage } from './CreateServiceAccountPage';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
index f73249196..d5d8687b7 100644
--- a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
+++ b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx
@@ -21,10 +21,15 @@ import {
useGetUserWsKey,
useRenameOrg,
useUpdateOrgUserRole,
- useUploadWsKey
+ useUploadWsKey,
} from '@app/hooks/api';
-import { OrgIncidentContactsTable, OrgMembersTable, OrgNameChangeSection } from './components';
+import {
+ OrgIncidentContactsTable,
+ OrgMembersTable,
+ OrgNameChangeSection,
+ OrgServiceAccountsTable
+} from './components';
export const OrgSettingsPage = () => {
const host = window.location.origin;
@@ -37,12 +42,11 @@ export const OrgSettingsPage = () => {
const { createNotification } = useNotificationContext();
const orgId = currentOrg?._id || '';
+
const { data: orgUsers, isLoading: isOrgUserLoading } = useGetOrgUsers(orgId);
- const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } =
- useGetUserWorkspaceMemberships(orgId);
+ const { data: workspaceMemberships, isLoading: IsWsMembershipLoading } = useGetUserWorkspaceMemberships(orgId);
const { data: wsKey } = useGetUserWsKey(currentWorkspace?._id || '');
- const { data: incidentContact, isLoading: IsIncidentContactLoading } =
- useGetOrgIncidentContact(orgId);
+ const { data: incidentContact, isLoading: IsIncidentContactLoading } = useGetOrgIncidentContact(orgId);
const renameOrg = useRenameOrg();
const removeUserOrgMembership = useDeleteOrgMembership();
@@ -52,12 +56,12 @@ export const OrgSettingsPage = () => {
const addIncidentContact = useAddIncidentContact();
const removeIncidentContact = useDeleteIncidentContact();
- const [completeInviteLink, setcompleteInviteLink] = useState("")
+ const [completeInviteLink, setcompleteInviteLink] = useState('');
const isMoreUsersNotAllowed =
(orgUsers || []).length >= 5 &&
subscriptionPlan === plans.starter &&
- host === 'https://app.infisical.com' &&
+ host === 'https://app.infisical.com' &&
currentWorkspace?._id !== '63ea8121b6e2b0543ba79616';
const onRenameOrg = async (name: string) => {
@@ -84,7 +88,7 @@ export const OrgSettingsPage = () => {
try {
await removeUserOrgMembership.mutateAsync({ orgId: currentOrg?._id, membershipId });
createNotification({
- text: 'Successfully removed used from org',
+ text: 'Successfully removed user from org',
type: 'success'
});
} catch (error) {
@@ -99,11 +103,14 @@ export const OrgSettingsPage = () => {
if (!currentOrg?._id) return;
try {
- const {data} = await addUserToOrg.mutateAsync({ organizationId: currentOrg?._id, inviteeEmail: email });
- setcompleteInviteLink(data?.completeInviteLink)
+ const { data } = await addUserToOrg.mutateAsync({
+ organizationId: currentOrg?._id,
+ inviteeEmail: email
+ });
+ setcompleteInviteLink(data?.completeInviteLink);
// only show this notification when email is configured. A [completeInviteLink] will not be sent if smtp is configured
- if (!data.completeInviteLink){
+ if (!data.completeInviteLink) {
createNotification({
text: 'Successfully invited user to the organization.',
type: 'success'
@@ -230,17 +237,15 @@ export const OrgSettingsPage = () => {
return (
-
-
-
{t('settings-org:title')}
-
- {t('settings-org:description')}
-
-
+
+
{t('settings-org:title')}
+
+ {t('settings-org:description')}
+
-
+
{t('section-members:org-members')}
@@ -259,6 +264,12 @@ export const OrgSettingsPage = () => {
setCompleteInviteLink={setcompleteInviteLink}
/>
+
+
+ Service Accounts
+
+
+
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
index e118e455b..58de0e288 100644
--- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgIncidentContactsTable/OrgIncidentContactsTable.tsx
@@ -27,7 +27,8 @@ import {
Td,
Th,
THead,
- Tr} from '@app/components/v2';
+ Tr
+} from '@app/components/v2';
import { usePopUp } from '@app/hooks';
import { useFetchServerStatus } from '@app/hooks/api/serverDetails';
import { IncidentContact } from '@app/hooks/api/types';
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
index 04ee9da3d..5c2a1fc44 100644
--- a/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgMembersTable/OrgMembersTable.tsx
@@ -103,7 +103,7 @@ export const OrgMembersTable = ({
() => members.find(({ user }) => userId === user?._id)?.role === 'owner',
[userId, members]
);
-
+
const filterdUser = useMemo(
() =>
members.filter(
@@ -132,25 +132,18 @@ export const OrgMembersTable = ({
placeholder="Search members..."
/>
-
- }
- onClick={() => {
- // if (serverDetails?.emailConfigured){
- if (isMoreUserNotAllowed) {
- handlePopUpOpen('upgradePlan');
- } else {
- reset();
- handlePopUpOpen('addMember');
- }
- // } else {
- // handlePopUpOpen('setUpEmail');
- // }
- }}
- >
- Add Member
-
-
+
}
+ onClick={() => {
+ if (isMoreUserNotAllowed) {
+ handlePopUpOpen('upgradePlan');
+ } else {
+ handlePopUpOpen('addMember');
+ }
+ }}
+ >
+ Add Member
+
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx
new file mode 100644
index 000000000..d788a6783
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/OrgServiceAccountsTable.tsx
@@ -0,0 +1,371 @@
+import { useEffect, useMemo,useState } from 'react';
+import { Controller,useForm } from 'react-hook-form';
+import { useRouter } from 'next/router';
+import {
+ faCheck,
+ faCopy,
+ faMagnifyingGlass,
+ faPencil,
+ faPlus,
+ faServer,
+ faTrash} from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { yupResolver } from '@hookform/resolvers/yup';
+import * as yup from 'yup';
+
+import { generateKeyPair } from '@app/components/utilities/cryptography/crypto';
+import {
+ Button,
+ DeleteActionModal,
+ EmptyState,
+ FormControl,
+ IconButton,
+ Input,
+ Modal,
+ ModalContent,
+ Select,
+ SelectItem,
+ Table,
+ TableContainer,
+ TableSkeleton,
+ TBody,
+ Td,
+ Th,
+ THead,
+ Tr
+} from '@app/components/v2';
+import { useOrganization, useWorkspace } from '@app/context';
+import { usePopUp, useToggle } from '@app/hooks';
+import {
+ useCreateServiceAccount,
+ useDeleteServiceAccount,
+ useGetServiceAccounts} from '@app/hooks/api';
+
+const serviceAccountExpiration = [
+ { label: '1 Day', value: 86400 },
+ { label: '7 Days', value: 604800 },
+ { label: '1 Month', value: 2592000 },
+ { label: '6 months', value: 15552000 },
+ { label: '12 months', value: 31104000 },
+ { label: 'Never', value: -1 }
+];
+
+const addServiceAccountFormSchema = yup.object({
+ name: yup.string().required().label('Name').trim(),
+ expiresIn: yup.string().required().label('Service Account Expiration')
+});
+
+type TAddServiceAccountForm = yup.InferType;
+
+export const OrgServiceAccountsTable = () => {
+ const router = useRouter();
+ const { currentOrg } = useOrganization();
+ const { currentWorkspace } = useWorkspace();
+
+ const orgId = currentOrg?._id || '';
+ const [step, setStep] = useState(0);
+ const [isAccessKeyCopied, setIsAccessKeyCopied] = useToggle(false);
+ const [isPublicKeyCopied, setIsPublicKeyCopied] = useToggle(false);
+ const [isPrivateKeyCopied, setIsPrivateKeyCopied] = useToggle(false);
+ const [accessKey, setAccessKey] = useState('');
+ const [publicKey, setPublicKey] = useState('');
+ const [privateKey, setPrivateKey] = useState('');
+ const [searchServiceAccountFilter, setSearchServiceAccountFilter] = useState('');
+ const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
+ 'addServiceAccount',
+ 'removeServiceAccount',
+ ] as const);
+
+ const { data: serviceAccounts = [], isLoading: isServiceAccountsLoading } = useGetServiceAccounts(orgId);
+
+ const createServiceAccount = useCreateServiceAccount();
+ const removeServiceAccount = useDeleteServiceAccount();
+
+ useEffect(() => {
+ let timer: NodeJS.Timeout;
+ if (isAccessKeyCopied) {
+ timer = setTimeout(() => setIsAccessKeyCopied.off(), 2000);
+ }
+
+ if (isPublicKeyCopied) {
+ timer = setTimeout(() => setIsPublicKeyCopied.off(), 2000);
+ }
+
+ if (isPrivateKeyCopied) {
+ timer = setTimeout(() => setIsPrivateKeyCopied.off(), 2000);
+ }
+
+ return () => clearTimeout(timer);
+ }, [isAccessKeyCopied, isPublicKeyCopied, isPrivateKeyCopied]);
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting }
+ } = useForm({ resolver: yupResolver(addServiceAccountFormSchema) });
+
+ const onAddServiceAccount = async ({ name, expiresIn }: TAddServiceAccountForm) => {
+ if (!currentOrg?._id) return;
+
+ const keyPair = generateKeyPair();
+ setPublicKey(keyPair.publicKey);
+ setPrivateKey(keyPair.privateKey);
+
+ const serviceAccountDetails = await createServiceAccount.mutateAsync({
+ name,
+ organizationId: currentOrg?._id,
+ publicKey: keyPair.publicKey,
+ expiresIn: Number(expiresIn)
+ });
+
+ setAccessKey(serviceAccountDetails.serviceAccountAccessKey);
+
+ setStep(1);
+ reset();
+ }
+
+ const onRemoveServiceAccount = async () => {
+ const serviceAccountId = (popUp?.removeServiceAccount?.data as { _id: string })?._id;
+ await removeServiceAccount.mutateAsync(serviceAccountId);
+ handlePopUpClose('removeServiceAccount');
+ }
+
+ const filteredServiceAccounts = useMemo(
+ () =>
+ serviceAccounts.filter(
+ ({ name }) =>
+ name.toLowerCase().includes(searchServiceAccountFilter)
+ ),
+ [serviceAccounts, searchServiceAccountFilter]
+ );
+
+ const renderStep = (stepToRender: number) => {
+ switch (stepToRender) {
+ case 0:
+ return (
+
+ );
+ case 1:
+ return (
+ <>
+ Access Key
+
+
{accessKey}
+
{
+ navigator.clipboard.writeText(accessKey);
+ setIsAccessKeyCopied.on();
+ }}
+ >
+
+
+ Copy
+
+
+
+ Public Key
+
+
{publicKey}
+
{
+ navigator.clipboard.writeText(publicKey);
+ setIsPublicKeyCopied.on();
+ }}
+ >
+
+
+ Copy
+
+
+
+ Private Key
+
+
{privateKey}
+
{
+ navigator.clipboard.writeText(privateKey);
+ setIsPrivateKeyCopied.on();
+ }}
+ >
+
+
+ Copy
+
+
+
+
+ >
+ );
+ default:
+ return
+ }
+ }
+
+ return (
+
+
+
+ setSearchServiceAccountFilter(e.target.value)}
+ leftIcon={ }
+ placeholder="Search service accounts..."
+ />
+
+
}
+ onClick={() => {
+ setStep(0);
+ reset();
+ handlePopUpOpen('addServiceAccount');
+ }}
+ >
+ Add Service Account
+
+
+
+
+
+ Name
+ Valid Until
+
+
+
+ {isServiceAccountsLoading && }
+ {!isServiceAccountsLoading && (
+ filteredServiceAccounts.map(({
+ name,
+ expiresAt,
+ _id: serviceAccountId
+ }) => {
+ return (
+
+ {name}
+ {new Date(expiresAt).toUTCString()}
+
+
+ {
+ if (currentWorkspace?._id) {
+ router.push(`/settings/org/${currentWorkspace._id}/service-accounts/${serviceAccountId}`);
+ }
+ }}
+ className="mr-2"
+ >
+
+
+ handlePopUpOpen('removeServiceAccount', { _id: serviceAccountId })}
+ >
+
+
+
+
+
+ );
+ })
+ )}
+ {!isServiceAccountsLoading && filteredServiceAccounts?.length === 0 && (
+
+
+
+
+
+ )}
+
+
+
+
{
+ handlePopUpToggle('addServiceAccount', isOpen);
+ reset();
+ }}
+ >
+
+ {renderStep(step)}
+
+
+
handlePopUpToggle('removeServiceAccount', isOpen)}
+ onDeleteApproved={onRemoveServiceAccount}
+ />
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/index.tsx
new file mode 100644
index 000000000..fb22cd2c4
--- /dev/null
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/OrgServiceAccountsTable/index.tsx
@@ -0,0 +1 @@
+export { OrgServiceAccountsTable } from './OrgServiceAccountsTable';
\ No newline at end of file
diff --git a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
index 15a09b0d2..bfe044235 100644
--- a/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
+++ b/frontend/src/views/Settings/OrgSettingsPage/components/index.tsx
@@ -1,3 +1,5 @@
export { OrgIncidentContactsTable } from './OrgIncidentContactsTable';
export { OrgMembersTable } from './OrgMembersTable';
export { OrgNameChangeSection } from './OrgNameChangeSection';
+export { OrgServiceAccountsTable } from './OrgServiceAccountsTable';
+
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
index 98019e0ad..cbfc1aeed 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx
@@ -64,7 +64,6 @@ export const SecretTagsSection = ({
});
const onFormSubmit = async (data: CreateWsTag) => {
- console.log(19191, data);
await onCreateTag(data);
handlePopUpClose('CreateSecretTag');
};
diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
index 429876435..10c95ee55 100644
--- a/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
+++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ServiceTokenSection/ServiceTokenSection.tsx
@@ -37,13 +37,14 @@ const apiTokenExpiry = [
{ label: '7 Days', value: 604800 },
{ label: '1 Month', value: 2592000 },
{ label: '6 months', value: 15552000 },
- { label: '12 months', value: 31104000 }
+ { label: '12 months', value: 31104000 },
+ { label: 'Never', value: null },
];
const createServiceTokenSchema = yup.object({
name: yup.string().required().label('Service Token Name'),
environment: yup.string().required().label('Environment'),
- expiresIn: yup.string().required().label('Service Token Name'),
+ expiresIn: yup.string().optional().label('Service Token Expiration'),
permissions: yup.object().shape({
read: yup.boolean().required(),
write: yup.boolean().required()
@@ -202,7 +203,7 @@ export const ServiceTokenSection = ({
defaultValue={String(apiTokenExpiry?.[0]?.value)}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
@@ -213,7 +214,7 @@ export const ServiceTokenSection = ({
className="w-full"
>
{apiTokenExpiry.map(({ label, value }) => (
-
+
{label}
))}
@@ -269,42 +270,6 @@ export const ServiceTokenSection = ({
);
}}
/>
- {/* {
- return (
- {
- onChange(state);
- }}
- >
- Read (default)
-
- );
- }}
- />
- {
- return (
- {
- onChange(state);
- }}
- >
- Write (optional)
-
- );
- }}
- /> */}
{row.name}
{row.environment}
- {new Date(row.expiresAt).toUTCString()}
+ {row.expiresAt && new Date(row.expiresAt).toUTCString()}
diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js
index 54f527c47..e8bd3b56b 100644
--- a/frontend/tailwind.config.js
+++ b/frontend/tailwind.config.js
@@ -1471,6 +1471,22 @@ module.exports = {
opacity: 1
// transform: "translateY(100%)",
}
+ },
+ drawerRightIn: {
+ '0%': {
+ transform: 'translateX(100%)'
+ },
+ '100%': {
+ transform: 'translateX(0)'
+ }
+ },
+ drawerRightOut: {
+ '0%': {
+ transform: 'translateX(0)'
+ },
+ '100%': {
+ transform: 'translateX(100%)'
+ }
}
},
animation: {
@@ -1478,6 +1494,9 @@ module.exports = {
// MODAL
fadeIn: 'fadeIn 100ms cubic-bezier(0.16, 1, 0.3, 1)',
popIn: 'popIn 150ms cubic-bezier(0.16, 1, 0.3, 1);',
+ // drawer
+ drawerRightIn: 'drawerRightIn 150ms ease-in-out',
+ drawerRightOut: 'drawerRightOut 150ms ease-in-out',
// Dropdown
slideDownAndFade: 'slideDownAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
slideLeftAndFade: 'slideLeftAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)',
diff --git a/helm-charts/README.md b/helm-charts/README.md
index 1bb587a2a..858f458bb 100644
--- a/helm-charts/README.md
+++ b/helm-charts/README.md
@@ -36,4 +36,4 @@ Steps to update the documentation :
1. `npm install ./readme-generator-for-helm`
1. `npm exec readme-generator -- --readme README.md --values values.yaml`
- It'll insert the table below the `## Parameters` title
- - It'll output errors if some of the path aren't documented
\ No newline at end of file
+ - It'll output errors if some of the path aren't documented
diff --git a/helm-charts/infisical/.gitignore b/helm-charts/infisical/.gitignore
index a2968aad7..c9fe0caa7 100644
--- a/helm-charts/infisical/.gitignore
+++ b/helm-charts/infisical/.gitignore
@@ -1,3 +1,4 @@
charts/
node_modules/
-package*.json
\ No newline at end of file
+package*.json
+*.bak
\ No newline at end of file
diff --git a/helm-charts/infisical/Chart.lock b/helm-charts/infisical/Chart.lock
index 3b5f48ca4..952009b7b 100644
--- a/helm-charts/infisical/Chart.lock
+++ b/helm-charts/infisical/Chart.lock
@@ -1,9 +1,12 @@
dependencies:
- name: mongodb
repository: https://charts.bitnami.com/bitnami
- version: 13.6.7
+ version: 13.9.4
- name: mailhog
repository: https://codecentric.github.io/helm-charts
version: 5.2.3
-digest: sha256:a54ae9ee60775f6f1aa916b59aee55b3ed5234b6bd88185fcb118b7f69539d70
-generated: "2023-02-13T14:13:27.525541038+01:00"
+- name: ingress-nginx
+ repository: https://kubernetes.github.io/ingress-nginx
+ version: 4.0.13
+digest: sha256:d1a679e6c30e37da96b7a4b6115e285f61e6ce0dd921ffbe2cf557418c229f33
+generated: "2023-04-08T15:59:12.950942-07:00"
diff --git a/helm-charts/infisical/Chart.yaml b/helm-charts/infisical/Chart.yaml
index 80fe48fcc..aaafc28ba 100644
--- a/helm-charts/infisical/Chart.yaml
+++ b/helm-charts/infisical/Chart.yaml
@@ -7,7 +7,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.1.15
+version: 0.1.17
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
@@ -17,10 +17,14 @@ appVersion: "1.17.0"
dependencies:
- name: mongodb
- version: "~13.6.7"
+ version: "~13.9.1"
repository: "https://charts.bitnami.com/bitnami"
condition: mongodb.enabled
- name: mailhog
version: "~5.2.3"
repository: "https://codecentric.github.io/helm-charts"
condition: mailhog.enabled
+ - name: ingress-nginx
+ version: 4.0.13
+ repository: https://kubernetes.github.io/ingress-nginx
+ condition: ingress.nginx.enabled
diff --git a/helm-charts/infisical/README.md b/helm-charts/infisical/README.md
index 4e20bbc86..e614d9600 100644
--- a/helm-charts/infisical/README.md
+++ b/helm-charts/infisical/README.md
@@ -6,7 +6,7 @@ This is the Infisical application Helm chart. This chart includes the following
| ---------- | ----------------------------------- |
| `frontend` | Infisical's Web UI |
| `backend` | Infisical's API |
-| `mongodb` | Infisical's local database |
+| `mongodb` | Infisical's database |
| `mailhog` | Infisical's development SMTP server |
## Installation
@@ -36,6 +36,19 @@ helm upgrade --install --atomic \
infisical infisical/infisical
```
+### Backup up encryption keys
+
+If you did not explicitly set required environment variables, this helm chart will auto-generated them by default. It's recommended to save these credentials somewhere safe. Run the following command in your cluster where Infisical chart is installed.
+
+This command requires [`jq`](https://stedolan.github.io/jq/download/)
+
+```sh
+# export secrets to a given file (requires jq)
+kubectl get secrets -n \
+ -o json | jq '.data | map_values(@base64d)' > \
+ .bak
+```
+
## Parameters
### Common parameters
@@ -68,34 +81,37 @@ helm upgrade --install --atomic \
### Infisical backend parameters
-| Name | Description | Value |
-| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- |
-| `backend.enabled` | Enable backend | `true` |
-| `backend.name` | Backend name | `backend` |
-| `backend.fullnameOverride` | Backend fullnameOverride | `""` |
-| `backend.podAnnotations` | Backend pod annotations | `{}` |
-| `backend.deploymentAnnotations` | Backend deployment annotations | `{}` |
-| `backend.replicaCount` | Backend replica count | `2` |
-| `backend.image.repository` | Backend image repository | `infisical/backend` |
-| `backend.image.tag` | Backend image tag | `latest` |
-| `backend.image.pullPolicy` | Backend image pullPolicy | `IfNotPresent` |
-| `backend.kubeSecretRef` | Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars)) | `""` |
-| `backend.service.annotations` | Backend service annotations | `{}` |
-| `backend.service.type` | Backend service type | `ClusterIP` |
-| `backend.service.nodePort` | Backend service nodePort (used if above type is `NodePort`) | `""` |
-| `backendEnvironmentVariables.ENCRYPTION_KEY` | **Required** Backend encryption key (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057)) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.JWT_SIGNUP_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057)) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.JWT_REFRESH_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057)) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.JWT_AUTH_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057)) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.JWT_SERVICE_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057)) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.SMTP_HOST` | **Required** Hostname to connect to for establishing SMTP connections | `MUST_REPLACE` |
-| `backendEnvironmentVariables.SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` |
-| `backendEnvironmentVariables.SMTP_SECURE` | If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` |
-| `backendEnvironmentVariables.SMTP_FROM_NAME` | Name label to be used in From field (e.g. Infisical) | `Infisical` |
-| `backendEnvironmentVariables.SMTP_FROM_ADDRESS` | **Required** Email address to be used for sending emails (e.g. dev@infisical.com) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.SMTP_USERNAME` | **Required** Credential to connect to host (e.g. team@infisical.com) | `MUST_REPLACE` |
-| `backendEnvironmentVariables.SMTP_PASSWORD` | **Required** Credential to connect to host | `MUST_REPLACE` |
-| `backendEnvironmentVariables.SITE_URL` | Absolute URL including the protocol (e.g. https://app.infisical.com) | `infisical.local` |
+| Name | Description | Value |
+| ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
+| `backend.enabled` | Enable backend | `true` |
+| `backend.name` | Backend name | `backend` |
+| `backend.fullnameOverride` | Backend fullnameOverride | `""` |
+| `backend.podAnnotations` | Backend pod annotations | `{}` |
+| `backend.deploymentAnnotations` | Backend deployment annotations | `{}` |
+| `backend.replicaCount` | Backend replica count | `2` |
+| `backend.image.repository` | Backend image repository | `infisical/backend` |
+| `backend.image.tag` | Backend image tag | `latest` |
+| `backend.image.pullPolicy` | Backend image pullPolicy | `IfNotPresent` |
+| `backend.kubeSecretRef` | Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars)) | `""` |
+| `backend.service.annotations` | Backend service annotations | `{}` |
+| `backend.service.type` | Backend service type | `ClusterIP` |
+| `backend.service.nodePort` | Backend service nodePort (used if above type is `NodePort`) | `""` |
+| `backendEnvironmentVariables.ENCRYPTION_KEY` | **Required** Backend encryption key (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.JWT_SIGNUP_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.JWT_REFRESH_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.JWT_AUTH_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.JWT_SERVICE_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.JWT_MFA_SECRET` | **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret) | `""` |
+| `backendEnvironmentVariables.SMTP_HOST` | **Required** Hostname to connect to for establishing SMTP connections | `""` |
+| `backendEnvironmentVariables.SMTP_PORT` | Port to connect to for establishing SMTP connections | `587` |
+| `backendEnvironmentVariables.SMTP_SECURE` | If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported | `false` |
+| `backendEnvironmentVariables.SMTP_FROM_NAME` | Name label to be used in From field (e.g. Infisical) | `Infisical` |
+| `backendEnvironmentVariables.SMTP_FROM_ADDRESS` | **Required** Email address to be used for sending emails (e.g. dev@infisical.com) | `""` |
+| `backendEnvironmentVariables.SMTP_USERNAME` | **Required** Credential to connect to host (e.g. team@infisical.com) | `""` |
+| `backendEnvironmentVariables.SMTP_PASSWORD` | **Required** Credential to connect to host | `""` |
+| `backendEnvironmentVariables.SITE_URL` | Absolute URL including the protocol (e.g. https://app.infisical.com) | `infisical.local` |
+| `backendEnvironmentVariables.INVITE_ONLY_SIGNUP` | To disable account creation from the login page (invites only) | `false` |
+| `backendEnvironmentVariables.MONGO_URL` | MongoDB connection string (external or internal)Leave it empty for auto-generated connection string | `""` |
### MongoDB(®) parameters
@@ -112,26 +128,42 @@ helm upgrade --install --atomic \
| `mongodb.image.repository` | MongoDB(®) image registry | `bitnami/mongodb` |
| `mongodb.image.tag` | MongoDB(®) image tag (immutable tags are recommended) | `6.0.4-debian-11-r0` |
| `mongodb.image.pullPolicy` | MongoDB(®) image pull policy | `IfNotPresent` |
+| `mongodb.livenessProbe.enabled` | Enable livenessProbe | `true` |
+| `mongodb.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `30` |
+| `mongodb.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `20` |
+| `mongodb.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `10` |
+| `mongodb.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `6` |
+| `mongodb.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` |
+| `mongodb.readinessProbe.enabled` | Enable readinessProbe | `true` |
+| `mongodb.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `5` |
+| `mongodb.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` |
+| `mongodb.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `10` |
+| `mongodb.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `6` |
+| `mongodb.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` |
| `mongodb.service.annotations` | Service annotations | `{}` |
| `mongodb.auth.enabled` | Enable custom authentication | `true` |
| `mongodb.auth.usernames` | Custom usernames list ([special characters warning](https://www.mongodb.com/docs/manual/reference/connection-string/#standard-connection-string-format)) | `["infisical"]` |
| `mongodb.auth.passwords` | Custom passwords list, match the above usernames order ([special characters warning](https://www.mongodb.com/docs/manual/reference/connection-string/#standard-connection-string-format)) | `["infisical"]` |
| `mongodb.auth.databases` | Custom databases list ([special characters warning](https://www.mongodb.com/docs/manual/reference/connection-string/#standard-connection-string-format)) | `["infisical"]` |
+| `mongodb.auth.rootUser` | Database root user name | `root` |
+| `mongodb.auth.rootPassword` | Database root user password | `root` |
| `mongodb.persistence.enabled` | Enable database persistence | `true` |
| `mongodb.persistence.existingClaim` | Existing persistent volume claim name | `""` |
| `mongodb.persistence.resourcePolicy` | Keep the persistent volume even on deletion (`keep` or `""`) | `keep` |
| `mongodb.persistence.accessModes` | Persistent volume access modes | `["ReadWriteOnce"]` |
| `mongodb.persistence.size` | Persistent storage request size | `8Gi` |
-| `mongodbConnection.externalMongoDBConnectionString` | External MongoDB connection string | `""` |
+| `mongodbConnection.externalMongoDBConnectionString` | Deprecated :warning: External MongoDB connection stringUse backendEnvironmentVariables.MONGO_URL instead | `""` |
### Ingress parameters
-| Name | Description | Value |
-| ------------------ | ------------------------------------------- | ----------------- |
-| `ingress.enabled` | Enable ingress | `true` |
-| `ingress.hostName` | Ingress hostname (your custom domain name) | `infisical.local` |
-| `ingress.tls` | Ingress TLS hosts (matching above hostName) | `[]` |
+| Name | Description | Value |
+| -------------------------- | ------------------------------------------------------------------------ | ------- |
+| `ingress.enabled` | Enable ingress | `true` |
+| `ingress.ingressClassName` | Ingress class name | `nginx` |
+| `ingress.annotations` | Ingress annotations | `{}` |
+| `ingress.hostName` | Ingress hostname (your custom domain name, e.g. `infisical.example.org`) | `""` |
+| `ingress.tls` | Ingress TLS hosts (matching above hostName) | `[]` |
### Mailhog parameters
@@ -152,7 +184,7 @@ helm upgrade --install --atomic \
| `mailhog.ingress.labels` | Ingress labels | `{}` |
| `mailhog.ingress.hosts[0].host` | Mailhog host | `mailhog.infisical.local` |
-Learn more in our [docs](https://infisical.com/docs/self-hosting/deployments/kubernetes)
+
## Persistence
@@ -185,32 +217,37 @@ Below example will deploy the following :
- The corresponding IP will depend on the tool or the way you're exposing the services ([learn more](https://minikube.sigs.k8s.io/docs/handbook/host-access/))
- [**mailhog.infisical.local**](https://mailhog.infisical.local)
- - Local SMTP server used to receive the signup verification code
+ - Local SMTP server used to receive the emails (e.g. signup verification code)
- You may have to add `mailhog.infisical.local` to your `/etc/hosts` or similar depending your OS
- The corresponding IP will depend on the tool or the way you're exposing the services ([learn more](https://minikube.sigs.k8s.io/docs/handbook/host-access/))
Use below values to setup a local development environment, adapt those variables as you need
+#### TL;DR
+
+If you're running a k8s cluster with `ingress-nginx`, you can run one of the below scripts :
+
+```sh
+# With 'kind' + 'helm', to create a local cluster and deploy the chart
+./examples.local-kind.sh
+
+# With 'helm' only, if you already have a cluster to deploy the chart
+./examples.local-helm.sh
+```
+
+#### Instructions
+
+Here's the step-by-step instructions to setup your local development environment. First create the below file :
+
```yaml
# values.dev.yaml
-# Enable all services for local development
-frontend:
- enabled: true
-backend:
- enabled: true
-mongodb:
- enabled: true
+# Enable mailhog for local development
mailhog:
enabled: true
# Configure backend development variables (required)
backendEnvironmentVariables:
- ENCRYPTION_KEY: 6c1fe4e407b8911c104518103505b218
- JWT_AUTH_SECRET: 4be6ba5602e0fa0ac6ac05c3cd4d247f
- JWT_REFRESH_SECRET: 5f2f3c8f0159068dc2bbb3a652a716ff
- JWT_SERVICE_SECRET: f32f716d70a42c5703f4656015e76200
- JWT_SIGNUP_SECRET: 3679e04ca949f914c03332aaaeba805a
SITE_URL: https://infisical.local
SMTP_FROM_ADDRESS: dev@infisical.local
SMTP_FROM_NAME: Local Infisical
@@ -240,6 +277,65 @@ helm upgrade --install --atomic \
## Upgrading
-### 1.15.0
+Find the chart upgrade instructions below. When upgrading from your version to one of the listed below, please follow every instructions in between.
-Refactoring in progress, instructions are coming soon
\ No newline at end of file
+Here's a snippet to upgrade your installation manually :
+
+```sh
+# replace below '' with your own values
+helm upgrade --install --atomic \
+ -n "" --create-namespace \
+ -f "" \
+ .
+```
+
+โน๏ธ Since we provide references to the k8s secret resources within the pods, their manifest file doesnt change and though doesnt reload (no changes detected). When upgrading your secrets, you'll have to do it through Helm (a timestamp field will be updated and your pods restarted)
+
+### 0.1.16
+
+- Auto-generation for the following variables, to ease your future upgrades or setups :
+ - `ENCRYPTION_KEY`
+ - `JWT_SIGNUP_SECRET`
+ - `JWT_REFRESH_SECRET`
+ - `JWT_AUTH_SECRET`
+ - `JWT_SERVICE_SECRET`
+ - `JWT_MFA_SECRET`
+
+We've migrated the applications' environment variables into `secrets` resources, shared within the deployments through `envFrom`. If you upgrade your installation make sure to backup your deployments' environment variables (e.g. encryption key and jwt secrets).
+
+The preference order is :
+- **user-defined** (values file or inline)
+ - **existing-secret** (for existing installations, you don't have to specify the secrets when upgrading if they already exist)
+ - **auto-generated** (if none of the values above have been found, we'll auto-generate a value for the user, only for the above mentioned variables)
+
+#### Instructions
+
+1. Make sure **you have all the required environment variables** defined in the value file (or inline `--set`) you'll provide to `helm`
+ 1. e.g. All the above mentioned variables
+1. **Backup your existing secrets** (safety precaution)
+ 1. with below [snippets](#snippets)
+1. **Upgrade the chart**, with the [instructions](#upgrading)
+ 1. It'll create a secret per service, and store the secrets/conf within (auto-generate if you don't provide the required ones)
+ 1. It'll link the secret to the deployment through `envFrom`
+ 1. It'll automatically remove the hard-coded `env.*` variables from your infisical deployments
+1. Make sure that the **created secrets match the ones in your backups**
+ 1. e.g. `kubectl get secret -n -backend --template={{.data.ENCRYPTION_KEY}} | base64 -d`
+1. You're all set!
+
+#### Snippets
+
+Here's some snippets to backup your current secrets **before the upgrade** (:warning: it requires [`jq`](https://stedolan.github.io/jq/download/)) :
+
+```sh
+# replace the below variables with yours (namespace + app)
+namespace=infisical; app=infisical; components="frontend backend"
+
+for component in $components; do
+ dpl=$(kubectl get deployment -n $namespace -l app=$app -l component=$component \
+ -o jsonpath="{.items[0].metadata.name}")
+
+ kubectl get deployments -n $namespace $dpl \
+ -o jsonpath='{.spec.template.spec.containers[0].env[*]}' | \
+ jq -r '.name + ":" + .value' > infisical-$component-conf.bak
+done
+```
\ No newline at end of file
diff --git a/helm-charts/infisical/examples/local-helm.sh b/helm-charts/infisical/examples/local-helm.sh
new file mode 100755
index 000000000..ac7466821
--- /dev/null
+++ b/helm-charts/infisical/examples/local-helm.sh
@@ -0,0 +1,37 @@
+#!/usr/bin/env bash
+
+## Infisical local k8s development environment setup script
+## using 'helm' and assume you already have a cluster and an ingress (nginx)
+##
+
+##
+## DEVELOPMENT USE ONLY
+## DO NOT USE IN PRODUCTION
+##
+
+# define variables
+cluster_name=infisical
+host=infisical.local
+
+# install infisical (local development)
+helm dep update
+cat < -n {{ .Release.Namespace }} \
+ -o json | jq '.data | map_values(@base64d)' > .bak
+
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
##
\ No newline at end of file
diff --git a/helm-charts/infisical/templates/_helpers.tpl b/helm-charts/infisical/templates/_helpers.tpl
index bf3f8e301..500edea33 100644
--- a/helm-charts/infisical/templates/_helpers.tpl
+++ b/helm-charts/infisical/templates/_helpers.tpl
@@ -122,8 +122,9 @@ Create the mongodb connection string.
{{- $pass := first .Values.mongodb.auth.passwords | default "root" -}}
{{- $database := first .Values.mongodb.auth.databases | default "test" -}}
{{- $connectionString := printf "mongodb://%s:%s@%s:%d/%s" $user $pass $host $port $database -}}
+{{/* Backward compatibility (< 0.1.16, deprecated) */}}
{{- if .Values.mongodbConnection.externalMongoDBConnectionString -}}
{{- $connectionString = .Values.mongodbConnection.externalMongoDBConnectionString -}}
{{- end -}}
{{- printf "%s" $connectionString -}}
-{{- end -}}
+{{- end -}}
\ No newline at end of file
diff --git a/helm-charts/infisical/templates/backend-deployment.yaml b/helm-charts/infisical/templates/backend-deployment.yaml
index f93f3c87e..797439019 100644
--- a/helm-charts/infisical/templates/backend-deployment.yaml
+++ b/helm-charts/infisical/templates/backend-deployment.yaml
@@ -1,31 +1,34 @@
+{{- $backend := .Values.backend }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "infisical.backend.fullname" . }}
- {{- with .Values.backend.deploymentAnnotations }}
annotations:
- {{- toYaml . | nindent 8 }}
- {{- end }}
+ updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
+ {{- with $backend.deploymentAnnotations }}
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
labels:
{{- include "infisical.backend.labels" . | nindent 4 }}
spec:
- replicas: {{ .Values.backend.replicaCount }}
+ replicas: {{ $backend.replicaCount }}
selector:
matchLabels:
{{- include "infisical.backend.matchLabels" . | nindent 6 }}
template:
- metadata:
+ metadata:
labels:
{{- include "infisical.backend.matchLabels" . | nindent 8 }}
- {{- with .Values.backend.podAnnotations }}
annotations:
+ updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
+ {{- with $backend.podAnnotations }}
{{- toYaml . | nindent 8 }}
- {{- end }}
+ {{- end }}
spec:
containers:
- - name: {{ template "infisical.name" . }}-{{ .Values.backend.name }}
- image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}"
- imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
+ - name: {{ template "infisical.name" . }}-{{ $backend.name }}
+ image: "{{ $backend.image.repository }}:{{ $backend.image.tag | default "latest" }}"
+ imagePullPolicy: {{ $backend.image.pullPolicy }}
readinessProbe:
httpGet:
path: /api/status
@@ -34,43 +37,58 @@ spec:
periodSeconds: 10
ports:
- containerPort: 4000
- {{- if .Values.backend.kubeSecretRef }}
envFrom:
- secretRef:
- name: {{ .Values.backend.kubeSecretRef }}
- {{- end }}
- env:
- - name: MONGO_URL
- value: {{ include "infisical.mongodb.connectionString" . | quote }}
- {{- if .Values.backendEnvironmentVariables }}
- {{- range $key, $value := .Values.backendEnvironmentVariables }}
- {{- if $value | quote | eq "MUST_REPLACE" }}
- {{ fail "Environment variables are not set. Please set all environment variables to continue." }}
- {{ end }}
- - name: {{ $key }}
- value: {{ quote $value }}
- {{- end }}
- {{- end }}
+ name: {{ $backend.kubeSecretRef | default (include "infisical.backend.fullname" .) }}
---
+
apiVersion: v1
kind: Service
metadata:
name: {{ include "infisical.backend.fullname" . }}
labels:
{{- include "infisical.backend.labels" . | nindent 4 }}
- {{- with .Values.backend.service.annotations }}
+ {{- with $backend.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
- type: {{ .Values.backend.service.type }}
+ type: {{ $backend.service.type }}
selector:
{{- include "infisical.backend.matchLabels" . | nindent 8 }}
ports:
- protocol: TCP
port: 4000
targetPort: 4000 # container port
- {{- if eq .Values.backend.service.type "NodePort" }}
- nodePort: {{ .Values.backend.service.nodePort }}
+ {{- if eq $backend.service.type "NodePort" }}
+ nodePort: {{ $backend.service.nodePort }}
{{- end }}
+
+---
+
+{{ if not $backend.kubeSecretRef }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "infisical.backend.fullname" . }}
+ annotations:
+ "helm.sh/resource-policy": "keep"
+type: Opaque
+stringData:
+ {{- $requiredVars := dict "ENCRYPTION_KEY" (randAlphaNum 32 | lower)
+ "JWT_SIGNUP_SECRET" (randAlphaNum 32 | lower)
+ "JWT_REFRESH_SECRET" (randAlphaNum 32 | lower)
+ "JWT_AUTH_SECRET" (randAlphaNum 32 | lower)
+ "JWT_SERVICE_SECRET" (randAlphaNum 32 | lower)
+ "JWT_MFA_SECRET" (randAlphaNum 32 | lower)
+ "MONGO_URL" (include "infisical.mongodb.connectionString" .) }}
+ {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "infisical.backend.fullname" .)) | default dict }}
+ {{- $secretData := (get $secretObj "data") | default dict }}
+ {{ range $key, $value := .Values.backendEnvironmentVariables }}
+ {{- $default := get $requiredVars $key -}}
+ {{- $current := get $secretData $key | b64dec -}}
+ {{- $v := $value | default ($current | default $default) -}}
+ {{ $key }}: {{ $v | quote }}
+ {{ end -}}
+{{- end }}
diff --git a/helm-charts/infisical/templates/frontend-deployment.yaml b/helm-charts/infisical/templates/frontend-deployment.yaml
index d396e5628..9c5cd1560 100644
--- a/helm-charts/infisical/templates/frontend-deployment.yaml
+++ b/helm-charts/infisical/templates/frontend-deployment.yaml
@@ -1,15 +1,17 @@
+{{- $frontend := .Values.frontend }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "infisical.frontend.fullname" . }}
- {{- with .Values.frontend.deploymentAnnotations }}
annotations:
- {{- toYaml . | nindent 8 }}
+ updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
+ {{- with .Values.frontend.deploymentAnnotations }}
+ {{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "infisical.frontend.labels" . | nindent 4 }}
spec:
- replicas: {{ .Values.frontend.replicaCount }}
+ replicas: {{ $frontend.replicaCount }}
selector:
matchLabels:
{{- include "infisical.frontend.matchLabels" . | nindent 6 }}
@@ -17,57 +19,70 @@ spec:
metadata:
labels:
{{- include "infisical.frontend.matchLabels" . | nindent 8 }}
- {{- with .Values.frontend.podAnnotations }}
annotations:
+ updatedAt: {{ now | date "2006-01-01 MST 15:04:05" | quote }}
+ {{- with $frontend.podAnnotations }}
{{- toYaml . | nindent 8 }}
- {{- end }}
+ {{- end }}
spec:
containers:
- - name: {{ template "infisical.name" . }}-{{ .Values.frontend.name }}
- image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}"
- imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
+ - name: {{ template "infisical.name" . }}-{{ $frontend.name }}
+ image: "{{ $frontend.image.repository }}:{{ $frontend.image.tag | default "latest" }}"
+ imagePullPolicy: {{ $frontend.image.pullPolicy }}
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
- {{- if .Values.frontend.kubeSecretRef }}
envFrom:
- secretRef:
- name: {{ .Values.frontend.kubeSecretRef }}
- {{- end }}
- {{- if .Values.frontendEnvironmentVariables }}
- env:
- {{- range $key, $value := .Values.frontendEnvironmentVariables }}
- {{- if $value | quote | eq "MUST_REPLACE" }}
- {{ fail "Environment variables are not set. Please set all environment variables to continue." }}
- {{ end }}
- - name: {{ $key }}
- value: {{ quote $value }}
- {{- end }}
- {{- end }}
+ name: {{ $frontend.kubeSecretRef | default (include "infisical.frontend.fullname" .) }}
ports:
- containerPort: 3000
+
---
+
apiVersion: v1
kind: Service
metadata:
name: {{ include "infisical.frontend.fullname" . }}
labels:
{{- include "infisical.frontend.labels" . | nindent 4 }}
- {{- with .Values.frontend.service.annotations }}
+ {{- with $frontend.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
- type: {{ .Values.frontend.service.type }}
+ type: {{ $frontend.service.type }}
selector:
{{- include "infisical.frontend.matchLabels" . | nindent 8 }}
ports:
- protocol: TCP
port: 3000 # service
targetPort: 3000 # container port
- {{- if eq .Values.frontend.service.type "NodePort" }}
- nodePort: {{ .Values.frontend.service.nodePort }}
- {{- end }}
\ No newline at end of file
+ {{- if eq $frontend.service.type "NodePort" }}
+ nodePort: {{ $frontend.service.nodePort }}
+ {{- end }}
+
+---
+
+{{ if not $frontend.kubeSecretRef }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "infisical.frontend.fullname" . }}
+ annotations:
+ "helm.sh/resource-policy": "keep"
+type: Opaque
+stringData:
+ {{- $requiredVars := dict }}
+ {{- $secretObj := (lookup "v1" "Secret" .Release.Namespace (include "infisical.frontend.fullname" .)) | default dict }}
+ {{- $secretData := (get $secretObj "data") | default dict }}
+ {{ range $key, $value := .Values.frontendEnvironmentVariables }}
+ {{- $default := get $requiredVars $key -}}
+ {{- $current := get $secretData $key | b64dec -}}
+ {{- $v := $value | default ($current | default $default) -}}
+ {{ $key }}: {{ $v | quote }}
+ {{ end -}}
+{{- end }}
\ No newline at end of file
diff --git a/helm-charts/infisical/templates/ingress.yaml b/helm-charts/infisical/templates/ingress.yaml
index cf9952ea3..a675fa2ff 100644
--- a/helm-charts/infisical/templates/ingress.yaml
+++ b/helm-charts/infisical/templates/ingress.yaml
@@ -1,16 +1,25 @@
{{ if .Values.ingress.enabled }}
+{{- $ingress := .Values.ingress }}
+{{- if and $ingress.ingressClassName (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
+ {{- if not (hasKey $ingress.annotations "kubernetes.io/ingress.class") }}
+ {{- $_ := set $ingress.annotations "kubernetes.io/ingress.class" $ingress.ingressClassName}}
+ {{- end }}
+{{- end }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: infisical-ingress
- {{- with .Values.ingress.annotations }}
+ {{- with $ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
-{{- if .Values.ingress.tls }}
+ {{- if and $ingress.ingressClassName (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
+ ingressClassName: {{ $ingress.ingressClassName | default "nginx" }}
+ {{- end }}
+{{- if $ingress.tls }}
tls:
- {{- range .Values.ingress.tls }}
+ {{- range $ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
@@ -19,21 +28,23 @@ spec:
{{- end }}
{{- end }}
rules:
- - host: {{ .Values.ingress.hostName}}
- http:
- paths:
- - path: {{ .Values.ingress.frontend.path }}
- pathType: {{ .Values.ingress.frontend.pathType }}
- backend:
- service:
- name: {{ include "infisical.frontend.fullname" . }}
- port:
- number: 3000
- - path: {{ .Values.ingress.backend.path }}
- pathType: {{ .Values.ingress.backend.pathType }}
- backend:
- service:
- name: {{ include "infisical.backend.fullname" . }}
- port:
- number: 4000
+ - http:
+ paths:
+ - path: {{ $ingress.frontend.path }}
+ pathType: {{ $ingress.frontend.pathType }}
+ backend:
+ service:
+ name: {{ include "infisical.frontend.fullname" . }}
+ port:
+ number: 3000
+ - path: {{ $ingress.backend.path }}
+ pathType: {{ $ingress.backend.pathType }}
+ backend:
+ service:
+ name: {{ include "infisical.backend.fullname" . }}
+ port:
+ number: 4000
+ {{- if $ingress.hostName }}
+ host: {{ $ingress.hostName }}
+ {{- end }}
{{ end }}
\ No newline at end of file
diff --git a/helm-charts/infisical/values.yaml b/helm-charts/infisical/values.yaml
index d569e7c31..392c9b3b2 100644
--- a/helm-charts/infisical/values.yaml
+++ b/helm-charts/infisical/values.yaml
@@ -46,6 +46,8 @@ frontend:
## @param frontend.kubeSecretRef Backend secret resource reference name (containing required [frontend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars))
##
kubeSecretRef: ""
+ ## Frontend service
+ ##
service:
## @param frontend.service.annotations Backend service annotations
##
@@ -103,6 +105,8 @@ backend:
## @param backend.kubeSecretRef Backend secret resource reference name (containing required [backend configuration variables](https://infisical.com/docs/self-hosting/configuration/envars))
##
kubeSecretRef: ""
+ ## Backend service
+ ##
service:
## @param backend.service.annotations Backend service annotations
##
@@ -118,20 +122,22 @@ backend:
## Documentation : https://infisical.com/docs/self-hosting/configuration/envars
##
backendEnvironmentVariables:
- ## @param backendEnvironmentVariables.ENCRYPTION_KEY **Required** Backend encryption key (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))
+ ## @param backendEnvironmentVariables.ENCRYPTION_KEY **Required** Backend encryption key (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
## Command to generate the required value (linux) : 'hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom', 'openssl rand -hex 16'
##
- ENCRYPTION_KEY: MUST_REPLACE
- ## @param backendEnvironmentVariables.JWT_SIGNUP_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))
- ## @param backendEnvironmentVariables.JWT_REFRESH_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))
- ## @param backendEnvironmentVariables.JWT_AUTH_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))
- ## @param backendEnvironmentVariables.JWT_SERVICE_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))
+ ENCRYPTION_KEY: ""
+ ## @param backendEnvironmentVariables.JWT_SIGNUP_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
+ ## @param backendEnvironmentVariables.JWT_REFRESH_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
+ ## @param backendEnvironmentVariables.JWT_AUTH_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
+ ## @param backendEnvironmentVariables.JWT_SERVICE_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
+ ## @param backendEnvironmentVariables.JWT_MFA_SECRET **Required** Secrets to sign JWT tokens (128-bit hex value, 32-characters hex, [example](https://stackoverflow.com/a/34329057))auto-generated variable (if not provided, and not found in an existing secret)
## Command to generate the required value (linux) : 'hexdump -vn16 -e'4/4 "%08X" 1 "\n"' /dev/urandom', 'openssl rand -hex 16'
##
- JWT_SIGNUP_SECRET: MUST_REPLACE
- JWT_REFRESH_SECRET: MUST_REPLACE
- JWT_AUTH_SECRET: MUST_REPLACE
- JWT_SERVICE_SECRET: MUST_REPLACE
+ JWT_SIGNUP_SECRET: ""
+ JWT_REFRESH_SECRET: ""
+ JWT_AUTH_SECRET: ""
+ JWT_SERVICE_SECRET: ""
+ JWT_MFA_SECRET: ""
## @param backendEnvironmentVariables.SMTP_HOST **Required** Hostname to connect to for establishing SMTP connections
## @param backendEnvironmentVariables.SMTP_PORT Port to connect to for establishing SMTP connections
## @param backendEnvironmentVariables.SMTP_SECURE If true, use TLS when connecting to host. If false, TLS will be used if STARTTLS is supported
@@ -140,16 +146,26 @@ backendEnvironmentVariables:
## @param backendEnvironmentVariables.SMTP_USERNAME **Required** Credential to connect to host (e.g. team@infisical.com)
## @param backendEnvironmentVariables.SMTP_PASSWORD **Required** Credential to connect to host
##
- SMTP_HOST: MUST_REPLACE
+ SMTP_HOST: ""
SMTP_PORT: 587
SMTP_SECURE: false
SMTP_FROM_NAME: Infisical
- SMTP_FROM_ADDRESS: MUST_REPLACE
- SMTP_USERNAME: MUST_REPLACE
- SMTP_PASSWORD: MUST_REPLACE
+ SMTP_FROM_ADDRESS: ""
+ SMTP_USERNAME: ""
+ SMTP_PASSWORD: ""
## @param backendEnvironmentVariables.SITE_URL Absolute URL including the protocol (e.g. https://app.infisical.com)
##
SITE_URL: infisical.local
+ ## @param backendEnvironmentVariables.INVITE_ONLY_SIGNUP To disable account creation from the login page (invites only)
+ ##
+ INVITE_ONLY_SIGNUP: false
+ ## @param backendEnvironmentVariables.MONGO_URL MongoDB connection string (external or internal)Leave it empty for auto-generated connection string
+ ## By default the backend will automatically be connected to a Mongo instance within the cluster
+ ## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
+ ## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
+ ## e.g. "mongodb://:@:/"
+ ##
+ MONGO_URL: ""
## @section MongoDB(®) parameters
## Documentation : https://github.com/bitnami/charts/blob/main/bitnami/mongodb/values.yaml
@@ -187,6 +203,38 @@ mongodb:
repository: bitnami/mongodb
pullPolicy: IfNotPresent
tag: "6.0.4-debian-11-r0"
+ ## Bitnami MongoDB(®) pods' liveness probe
+ ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes
+ ## @param mongodb.livenessProbe.enabled Enable livenessProbe
+ ## @param mongodb.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe
+ ## @param mongodb.livenessProbe.periodSeconds Period seconds for livenessProbe
+ ## @param mongodb.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe
+ ## @param mongodb.livenessProbe.failureThreshold Failure threshold for livenessProbe
+ ## @param mongodb.livenessProbe.successThreshold Success threshold for livenessProbe
+ ##
+ livenessProbe:
+ enabled: true
+ initialDelaySeconds: 30
+ periodSeconds: 20
+ timeoutSeconds: 10
+ failureThreshold: 6
+ successThreshold: 1
+ ## Bitnami MongoDB(®) pods' readiness probe. Evaluated as a template.
+ ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes
+ ## @param mongodb.readinessProbe.enabled Enable readinessProbe
+ ## @param mongodb.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe
+ ## @param mongodb.readinessProbe.periodSeconds Period seconds for readinessProbe
+ ## @param mongodb.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe
+ ## @param mongodb.readinessProbe.failureThreshold Failure threshold for readinessProbe
+ ## @param mongodb.readinessProbe.successThreshold Success threshold for readinessProbe
+ ##
+ readinessProbe:
+ enabled: true
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ timeoutSeconds: 10
+ failureThreshold: 6
+ successThreshold: 1
## @param mongodb.service.annotations Service annotations
##
service:
@@ -209,8 +257,12 @@ mongodb:
##
databases:
- "infisical"
- rootPassword: root
+ ## @param mongodb.auth.rootUser Database root user name
+ ##
rootUser: root
+ ## @param mongodb.auth.rootPassword Database root user password
+ ##
+ rootPassword: root
## MongoDB persistence configuration
##
persistence:
@@ -230,7 +282,7 @@ mongodb:
##
size: 8Gi
-## @param mongodbConnection.externalMongoDBConnectionString External MongoDB connection string
+## @param mongodbConnection.externalMongoDBConnectionString Deprecated :warning: External MongoDB connection stringUse backendEnvironmentVariables.MONGO_URL instead
## By default the backend will be connected to a Mongo instance within the cluster
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
@@ -246,15 +298,23 @@ ingress:
## @param ingress.enabled Enable ingress
##
enabled: true
+ ## @param ingress.ingressClassName Ingress class name
+ ##
+ ingressClassName: nginx
+ ## @param ingress.nginx.enabled Ingress controller
+ ##
+ nginx:
+ enabled: false
+ ## @param ingress.annotations Ingress annotations
+ ##
annotations:
- ## @skip ingress.annotations.kubernetes.io/ingress.class
- ##
- kubernetes.io/ingress.class: "nginx"
+ {}
+ # kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx
- ## @param ingress.hostName Ingress hostname (your custom domain name)
+ ## @param ingress.hostName Ingress hostname (your custom domain name, e.g. `infisical.example.org`)
## Replace with your own domain
##
- hostName: infisical.local
+ hostName: ""
## @skip ingress.frontend
##
frontend:
diff --git a/nginx/default.conf b/nginx/default.conf
index f52edda3f..4c0e2434d 100644
--- a/nginx/default.conf
+++ b/nginx/default.conf
@@ -11,7 +11,8 @@ server {
proxy_pass http://backend:4000;
proxy_redirect off;
- proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
+ # proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
+ proxy_cookie_path / "/; HttpOnly; SameSite=strict";
}
location / {