diff --git a/.github/workflows/helm_chart_release.yml b/.github/workflows/helm_chart_release.yml new file mode 100644 index 0000000000..8f47da69d0 --- /dev/null +++ b/.github/workflows/helm_chart_release.yml @@ -0,0 +1,22 @@ +name: Release Helm Charts + +on: [workflow_dispatch] + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Install Helm + uses: azure/setup-helm@v3 + with: + version: v3.10.0 + - name: Install python + uses: actions/setup-python@v4 + - name: Install Cloudsmith CLI + run: pip install --upgrade cloudsmith-cli + - name: Build and push helm package to Cloudsmith + run: cd helm-charts && sh upload-to-cloudsmith.sh + env: + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/release_build.yml b/.github/workflows/release_build.yml index aea82a9d2b..695b0ea240 100644 --- a/.github/workflows/release_build.yml +++ b/.github/workflows/release_build.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: Go releaser on: push: diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 410c442b35..e26b9b3606 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -106,7 +106,7 @@ const initializeDefaultOrg = async ({ // initialize a default workspace inside the new organization const workspace = await createWorkspace({ - name: `${user.firstName}'s Project`, + name: `Example Project`, organizationId: organization._id.toString() }); diff --git a/cli/packages/cmd/root.go b/cli/packages/cmd/root.go index 826ccb4df6..42d85e5b25 100644 --- a/cli/packages/cmd/root.go +++ b/cli/packages/cmd/root.go @@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{ Short: "Infisical CLI is used to inject environment variables into any process", Long: `Infisical is a simple, end-to-end encrypted service that enables teams to sync and manage their environment variables across their development life cycle.`, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - Version: "0.1.5", + Version: "0.1.6", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/docs/contributing/FAQ.mdx b/docs/contributing/FAQ.mdx new file mode 100644 index 0000000000..be65f5ca7e --- /dev/null +++ b/docs/contributing/FAQ.mdx @@ -0,0 +1,16 @@ +--- +title: "Frequently Asked Questions" +description: "Have any questions? [Join our Slack community](https://join.slack.com/t/infisical-users/shared_invite/zt-1kdbk07ro-RtoyEt_9E~fyzGo_xQYP6g)." +--- + +## Problem with SMTP + +You can normally populate `SMTP_USERNAME` and `SMTP_PASSWORD` with your usual login and password (you could also create a 'burner' email). Sometimes, there still are problems. + +You can go to your Gmail account settings > security and enable “less secure apps”. This would allow Infisical to use your Gmail to send emails. + +If it still doesn't work, [this](https://stackoverflow.com/questions/72547853/unable-to-send-email-in-c-sharp-less-secure-app-access-not-longer-available/72553362#72553362) should help. + +## `MONGO_URL` issues + +Your `MONGO_URL` should be something like `mongodb://root:example@mongo:27017/?authSource=admin`. If you want to change it (not recommended), you should make sure that you keep this URL in line with `MONGO_USERNAME=root` and `MONGO_PASSWORD=example`. \ No newline at end of file diff --git a/docs/mint.json b/docs/mint.json index 7d0317f0ef..1d52466808 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -102,8 +102,11 @@ "pages": [ "self-hosting/overview", { - "group": "Deployments", - "pages": ["self-hosting/deployments/linux"] + "group": "Deployments options", + "pages": [ + "self-hosting/deployments/linux", + "self-hosting/deployments/kubernetes" + ] }, { "group": "Configuration", @@ -162,7 +165,8 @@ "pages": [ "contributing/overview", "contributing/code-of-conduct", - "contributing/developing" + "contributing/developing", + "contributing/FAQ" ] } ], diff --git a/docs/self-hosting/deployments/kubernetes.mdx b/docs/self-hosting/deployments/kubernetes.mdx new file mode 100644 index 0000000000..8ed2c0e056 --- /dev/null +++ b/docs/self-hosting/deployments/kubernetes.mdx @@ -0,0 +1,54 @@ +--- +title: "Kubernetes" +description: "Deploy with Kubernetes" +--- + + +Self-host vs. Infisical Cloud + +Self-hosting Infisical means managing the service yourself, taking care of upgrades, scaling, security, etc. + +If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud. + + + +**Prerequisites** +- You have understanding of [Kubernetes](https://kubernetes.io/) +- You have understanding of [Helm package manager](https://helm.sh/) +- You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster + + +#### 1. Fill our environment variables + +Before you can deploy the Helm chart, you must fill out the required environment variables. To do so, please either download or copy the +contents of [this file](https://raw.githubusercontent.com/Infisical/infisical/main/helm-charts/infisical/values.yaml) to a `.yaml` file. +_Refer to the available [environment variables](../../self-hosting/configuration/envars)_ + +Once you have a local copy of the values file, fill our the required environment variables and save the file. + + +#### 2. Install Infisical Helm repository + +```bash +helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + +helm repo update +``` + +#### 3. Install the Helm chart + +By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify +that by adding the `--namespace ` to your `helm install` command. + +```bash +## Installs to default namespace +helm install infisical-helm-charts/infisical --values +``` + + +If you have not filled out all of the required environment variables, you will see an error message prompting you to +do so. + + +4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. +Note: Please allow an additional time (2 minutes) for the frontend pods to be fully ready. \ No newline at end of file diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index 8f2df5c378..b0729637cb 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -9,17 +9,22 @@ Self-hosting Infisical means managing the service yourself, taking care of upgra If you're less technical and looking for a hands-free experience with minimal overhead then we recommend Infisical Cloud. -Infisical Cloud also comes with some extra features unavailabe in the self-hosted edition. You can find more information about Infisical Cloud's offering on the pricing page. +Infisical Cloud also comes with some extra features unavailable in the self-hosted edition. You can find more information about Infisical Cloud's offering on the pricing page. ## Deployment options -Infisical can be deployed on a Linux VM with docker-compose. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon. +Infisical can be deployed on a Linux VM with docker-compose and Kubernetes. We're rolling out more specific deployment options for DigitalOcean, AWS, GCP, and Azure soon. -Options: - -- [Linux VM](/self-hosting/deployments/linux) + + + Deploy to any Linux with Docker + + + Deploy to your Kubernetes cluster + + ## Telemetry diff --git a/frontend/.eslintrc b/frontend/.eslintrc index cfb3f694bb..b562aaa1fc 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -10,7 +10,12 @@ "rules": { "react-hooks/exhaustive-deps": "off", "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "simple-import-sort/exports": "warn", "simple-import-sort/imports": [ "warn", diff --git a/frontend/components/basic/InputField.js b/frontend/components/basic/InputField.tsx similarity index 89% rename from frontend/components/basic/InputField.js rename to frontend/components/basic/InputField.tsx index 8368d97976..02bb8a8cc8 100644 --- a/frontend/components/basic/InputField.js +++ b/frontend/components/basic/InputField.tsx @@ -1,19 +1,27 @@ -import React from "react"; -import { useState } from "react"; +import React, { useState } from "react"; import { useRouter } from "next/router"; -import { - faCircle, - faCircleExclamation, - faE, - faEye, - faEyeSlash, -} from "@fortawesome/free-solid-svg-icons"; +import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import guidGenerator from "../utilities/randomId"; -import Error from "./Error"; -const InputField = (props) => { +interface InputFieldProps { + static?: boolean; + label: string; + type: string; + value: string; + placeholder?: string; + isRequired: boolean; + disabled?: boolean; + error?: boolean; + text?: string; + name?: string; + blurred?: boolean; + errorText?: string; + onChangeHandler: (value: string) => void; +} + +const InputField = (props: InputFieldProps) => { const [passwordVisible, setPasswordVisible] = useState(false); const router = useRouter(); @@ -67,7 +75,7 @@ const InputField = (props) => { > props.onChangeHandler(e.target.value)} - type={passwordVisible == false ? props.type : "text"} + type={passwordVisible === false ? props.type : "text"} placeholder={props.placeholder} value={props.value} required={props.isRequired} diff --git a/frontend/components/basic/layout.js b/frontend/components/basic/Layout.tsx similarity index 55% rename from frontend/components/basic/layout.js rename to frontend/components/basic/Layout.tsx index 63dbf5dea5..4a40fdef08 100644 --- a/frontend/components/basic/layout.js +++ b/frontend/components/basic/Layout.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-unexpected-multiline */ /* eslint-disable react-hooks/exhaustive-deps */ import { useEffect, useState } from "react"; import Link from "next/link"; @@ -19,8 +20,10 @@ import getOrganizationUsers from "~/pages/api/organization/GetOrgUsers"; import addUserToWorkspace from "~/pages/api/workspace/addUserToWorkspace"; import createWorkspace from "~/pages/api/workspace/createWorkspace"; import getWorkspaces from "~/pages/api/workspace/getWorkspaces"; +import uploadKeys from "~/pages/api/workspace/uploadKeys"; import NavBarDashboard from "../navigation/NavBarDashboard"; +import { tempLocalStorage } from "../utilities/checks/tempLocalStorage"; import { decryptAssymmetric, encryptAssymmetric, @@ -29,13 +32,17 @@ import Button from "./buttons/Button"; import AddWorkspaceDialog from "./dialog/AddWorkspaceDialog"; import Listbox from "./Listbox"; -export default function Layout({ children }) { +interface LayoutProps { + children: React.ReactNode; +} + +export default function Layout({ children }: LayoutProps) { const router = useRouter(); const [workspaceList, setWorkspaceList] = useState([]); const [workspaceMapping, setWorkspaceMapping] = useState([{ 1: 2 }]); const [workspaceSelected, setWorkspaceSelected] = useState("∞"); - let [newWorkspaceName, setNewWorkspaceName] = useState(""); - let [isOpen, setIsOpen] = useState(false); + const [newWorkspaceName, setNewWorkspaceName] = useState(""); + const [isOpen, setIsOpen] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); @@ -43,165 +50,186 @@ export default function Layout({ children }) { setIsOpen(false); } + function openModal() { + setIsOpen(true); + } + // TODO: what to do about the fact that 2ids can have the same name /** * When a user creates a new workspace, redirect them to the page of the new workspace. * @param {*} workspaceName */ - async function submitModal(workspaceName, addAllUsers) { + async function submitModal(workspaceName: string, addAllUsers: boolean) { setLoading(true); + // timeout code. setTimeout(() => setLoading(false), 1500); - const workspaces = await getWorkspaces(); - const currentWorkspaces = workspaces.map((workspace) => workspace.name); - if (!currentWorkspaces.includes(workspaceName)) { - const newWorkspace = await createWorkspace( - workspaceName, - localStorage.getItem("orgData.id") - ); - let newWorkspaceId; - try { - newWorkspaceId = newWorkspace._id; - } catch (error) { - console.log(error); - } - if (addAllUsers) { - let orgUsers = await getOrganizationUsers({ - orgId: localStorage.getItem("orgData.id"), + + try { + const workspaces = await getWorkspaces(); + const currentWorkspaces = workspaces.map((workspace) => workspace.name); + if (!currentWorkspaces.includes(workspaceName)) { + const newWorkspace = await createWorkspace({ + workspaceName, + organizationId: tempLocalStorage("orgData.id"), }); - orgUsers.map(async (user) => { - if (user.status == "accepted") { - let result = await addUserToWorkspace( - user.user.email, - newWorkspaceId - ); - if (result?.invitee && result?.latestKey) { - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); + const newWorkspaceId = newWorkspace._id; - // assymmetrically decrypt symmetric key with local private key - const key = decryptAssymmetric({ - ciphertext: result.latestKey.encryptedKey, - nonce: result.latestKey.nonce, - publicKey: result.latestKey.sender.publicKey, - privateKey: PRIVATE_KEY, - }); + if (addAllUsers) { + const orgUsers = await getOrganizationUsers({ + orgId: tempLocalStorage("orgData.id"), + }); + orgUsers.map(async (user: any) => { + if (user.status == "accepted") { + const result = await addUserToWorkspace( + user.user.email, + newWorkspaceId + ); + if (result?.invitee && result?.latestKey) { + const PRIVATE_KEY = tempLocalStorage("PRIVATE_KEY"); - const { ciphertext, nonce } = encryptAssymmetric({ - plaintext: key, - publicKey: result.invitee.publicKey, - privateKey: PRIVATE_KEY, - }); + // assymmetrically decrypt symmetric key with local private key + const key = decryptAssymmetric({ + ciphertext: result.latestKey.encryptedKey, + nonce: result.latestKey.nonce, + publicKey: result.latestKey.sender.publicKey, + privateKey: PRIVATE_KEY, + }); - uploadKeys(newWorkspaceId, result.invitee._id, ciphertext, nonce); + const { ciphertext, nonce } = encryptAssymmetric({ + plaintext: key, + publicKey: result.invitee.publicKey, + privateKey: PRIVATE_KEY, + }) as { ciphertext: string; nonce: string }; + + uploadKeys( + newWorkspaceId, + result.invitee._id, + ciphertext, + nonce + ); + } } - } - }); + }); + } + router.push("/dashboard/" + newWorkspaceId + "?Development"); + setIsOpen(false); + setNewWorkspaceName(""); + } else { + console.error("A project with this name already exists."); + setError(true); + setLoading(false); } - router.push("/dashboard/" + newWorkspaceId + "?Development"); - setIsOpen(false); - setNewWorkspaceName(""); - } else { - setError("A project with this name already exists."); + } catch (err) { + console.error(err); + setError(true); setLoading(false); } } - function openModal() { - setIsOpen(true); - } - const menuItems = [ { href: - "/dashboard/" + workspaceMapping[workspaceSelected] + "?Development", + "/dashboard/" + + workspaceMapping[workspaceSelected as any] + + "?Development", title: "Secrets", emoji: , }, { - href: "/users/" + workspaceMapping[workspaceSelected], + href: "/users/" + workspaceMapping[workspaceSelected as any], title: "Members", emoji: , }, { - href: "/integrations/" + workspaceMapping[workspaceSelected], + href: "/integrations/" + workspaceMapping[workspaceSelected as any], title: "Integrations", emoji: , }, { - href: "/settings/project/" + workspaceMapping[workspaceSelected], + href: "/settings/project/" + workspaceMapping[workspaceSelected as any], title: "Project Settings", emoji: , }, ]; - useEffect(async () => { + useEffect(() => { // Put a user in a workspace if they're not in one yet - if ( - localStorage.getItem("orgData.id") == null || - localStorage.getItem("orgData.id") == "" - ) { - const userOrgs = await getOrganizations(); - localStorage.setItem("orgData.id", userOrgs[0]._id); - } - - let orgUserProjects = await getOrganizationUserProjects({ - orgId: localStorage.getItem("orgData.id"), - }); - let userWorkspaces = orgUserProjects; - if ( - userWorkspaces.length == 0 && - router.asPath != "/noprojects" && - !router.asPath.includes("settings") - ) { - router.push("/noprojects"); - } else if (router.asPath != "/noprojects") { - const intendedWorkspaceId = router.asPath - .split("/") - [router.asPath.split("/").length - 1].split("?")[0]; + const putUserInWorkSpace = async () => { + if (tempLocalStorage("orgData.id") === "") { + const userOrgs = await getOrganizations(); + localStorage.setItem("orgData.id", userOrgs[0]._id); + } - // If a user is not a member of a workspace they are trying to access, just push them to one of theirs + const orgUserProjects = await getOrganizationUserProjects({ + orgId: tempLocalStorage("orgData.id"), + }); + const userWorkspaces = orgUserProjects; if ( - intendedWorkspaceId != "heroku" && - !userWorkspaces - .map((workspace) => workspace._id) - .includes(intendedWorkspaceId) + userWorkspaces.length == 0 && + router.asPath != "/noprojects" && + !router.asPath.includes("settings") ) { - router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); - } else { - setWorkspaceList(userWorkspaces.map((workspace) => workspace.name)); - setWorkspaceMapping( - Object.fromEntries( - userWorkspaces.map((workspace) => [workspace.name, workspace._id]) - ) - ); - setWorkspaceSelected( - Object.fromEntries( - userWorkspaces.map((workspace) => [workspace._id, workspace.name]) - )[ - router.asPath - .split("/") - [router.asPath.split("/").length - 1].split("?")[0] - ] - ); + router.push("/noprojects"); + } else if (router.asPath != "/noprojects") { + const intendedWorkspaceId = router.asPath + .split("/") + [router.asPath.split("/").length - 1].split("?")[0]; + // If a user is not a member of a workspace they are trying to access, just push them to one of theirs + if ( + intendedWorkspaceId != "heroku" && + !userWorkspaces + .map((workspace: { _id: string }) => workspace._id) + .includes(intendedWorkspaceId) + ) { + router.push("/dashboard/" + userWorkspaces[0]._id + "?Development"); + } else { + setWorkspaceList( + userWorkspaces.map((workspace: any) => workspace.name) + ); + setWorkspaceMapping( + Object.fromEntries( + userWorkspaces.map((workspace: any) => [ + workspace.name, + workspace._id, + ]) + ) as any + ); + setWorkspaceSelected( + Object.fromEntries( + userWorkspaces.map((workspace: any) => [ + workspace._id, + workspace.name, + ]) + )[ + router.asPath + .split("/") + [router.asPath.split("/").length - 1].split("?")[0] + ] + ); + } } - } + }; + putUserInWorkSpace(); }, []); useEffect(() => { try { if ( - workspaceMapping[workspaceSelected] && - workspaceMapping[workspaceSelected] !== + workspaceMapping[Number(workspaceSelected)] && + `${workspaceMapping[Number(workspaceSelected)]}` !== router.asPath .split("/") [router.asPath.split("/").length - 1].split("?")[0] ) { router.push( - "/dashboard/" + workspaceMapping[workspaceSelected] + "?Development" + "/dashboard/" + + workspaceMapping[Number(workspaceSelected)] + + "?Development" ); localStorage.setItem( "projectData.id", - workspaceMapping[workspaceSelected] + `${workspaceMapping[Number(workspaceSelected)]}` ); } } catch (error) { @@ -218,18 +246,18 @@ export default function Layout({ children }) {