diff --git a/.github/workflows/release_docker_k8_operator.yaml b/.github/workflows/release_docker_k8_operator.yaml index 788d414b6..517549ea8 100644 --- a/.github/workflows/release_docker_k8_operator.yaml +++ b/.github/workflows/release_docker_k8_operator.yaml @@ -1,10 +1,16 @@ -name: Release Docker image for K8 operator -on: [workflow_dispatch] +name: Release Docker image for K8 operator +on: + push: + tags: + - "infisical-k8-operator/v*.*.*" jobs: release: runs-on: ubuntu-latest steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}" - uses: actions/checkout@v2 - name: 🔧 Set up QEMU @@ -26,4 +32,6 @@ jobs: context: k8-operator push: true platforms: linux/amd64,linux/arm64 - tags: infisical/kubernetes-operator:latest \ No newline at end of file + tags: | + infisical/kubernetes-operator:latest + infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }} diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index fb13a6575..652fc4b32 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -27,6 +27,30 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { }); } +/** + * Return checkout url for pro trial + * @param req + * @param res + * @returns + */ +export const startOrganizationTrial = async (req: Request, res: Response) => { + const { organizationId } = req.params; + const { success_url } = req.body; + + const { data: { url } } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`, + { + success_url + } + ); + + EELicenseService.delPlan(organizationId); + + return res.status(200).send({ + url + }); +} + /** * Return the organization's current plan's billing info * @param req diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 37be3a96d..c9104d964 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -41,6 +41,21 @@ router.get( organizationsController.getOrganizationPlan ); +router.post( + "/:organizationId/session/trial", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + body("success_url").exists().trim(), + validateRequest, + organizationsController.startOrganizationTrial +); + router.get( "/:organizationId/plan/billing", requireAuth({ diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index b4bb97753..88220933d 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -32,6 +32,7 @@ interface FeatureSet { auditLogs: boolean; status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null; trial_end: number | null; + has_used_trial: boolean; } /** @@ -63,7 +64,8 @@ class EELicenseService { customAlerts: true, auditLogs: false, status: null, - trial_end: null + trial_end: null, + has_used_trial: true } public localFeatureSet: NodeCache; @@ -71,7 +73,7 @@ class EELicenseService { constructor() { this._isLicenseValid = true; this.localFeatureSet = new NodeCache({ - stdTTL: 300, + stdTTL: 60, }); } @@ -112,6 +114,12 @@ class EELicenseService { await this.getPlan(organizationId, workspaceId); } } + + public async delPlan(organizationId: string) { + if (this.instanceType === "cloud") { + this.localFeatureSet.del(`${organizationId}-`); + } + } public async initGlobalFeatureSet() { const licenseServerKey = await getLicenseServerKey(); diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 4bd58020b..b067d499a 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -2,9 +2,10 @@ import nodemailer from "nodemailer"; import { SMTP_HOST_GMAIL, SMTP_HOST_MAILGUN, + SMTP_HOST_OFFICE365, SMTP_HOST_SENDGRID, SMTP_HOST_SOCKETLABS, - SMTP_HOST_ZOHOMAIL, + SMTP_HOST_ZOHOMAIL } from "../variables"; import SMTPConnection from "nodemailer/lib/smtp-connection"; import * as Sentry from "@sentry/node"; @@ -15,6 +16,7 @@ import { getSmtpSecure, getSmtpUsername, } from "../config"; +import { getLogger } from "../utils/logger"; export const initSmtp = async () => { const mailOpts: SMTPConnection.Options = { @@ -58,6 +60,12 @@ export const initSmtp = async () => { ciphers: "TLSv1.2", } break; + case SMTP_HOST_OFFICE365: + mailOpts.requireTLS = true; + mailOpts.tls = { + ciphers: "TLSv1.2" + } + break; default: if ((await getSmtpHost()).includes("amazonaws.com")) { mailOpts.tls = { @@ -73,10 +81,12 @@ export const initSmtp = async () => { const transporter = nodemailer.createTransport(mailOpts); transporter .verify() - .then((err) => { + .then(async () => { Sentry.setUser(null); Sentry.captureMessage("SMTP - Successfully connected"); - console.log("SMTP - Successfully connected") + (await getLogger("backend-main")).info( + "SMTP - Successfully connected" + ); }) .catch(async (err) => { Sentry.setUser(null); diff --git a/backend/src/variables/smtp.ts b/backend/src/variables/smtp.ts index 8a0e752eb..4ad68c356 100644 --- a/backend/src/variables/smtp.ts +++ b/backend/src/variables/smtp.ts @@ -3,3 +3,4 @@ export const SMTP_HOST_MAILGUN = "smtp.mailgun.org"; export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com"; export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com"; export const SMTP_HOST_GMAIL = "smtp.gmail.com"; +export const SMTP_HOST_OFFICE365 = "smtp.office365.com"; \ No newline at end of file diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index a94be0ab0..5afec6782 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -246,7 +246,11 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req } if response.IsError() { - return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + if response.StatusCode() == 401 { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Request to access secrets with [environment=%v] [path=%v] [workspaceId=%v] is denied. Please check if your authentication method has access to requested scope", request.Environment, request.SecretPath, request.WorkspaceId) + } else { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%v]", response.RawResponse) + } } return secretsResponse, nil diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index c629130a4..f8d397233 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -73,7 +73,6 @@ var loginCmd = &cobra.Command{ return } } - //override domain domainQuery := true if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { @@ -322,6 +321,8 @@ func DomainOverridePrompt() (bool, error) { ) options := []string{PRESET, OVERRIDE} + //trim the '/' from the end of the domain url + config.INFISICAL_URL_MANUAL_OVERRIDE = strings.TrimRight(config.INFISICAL_URL_MANUAL_OVERRIDE, "/") optionsPrompt := promptui.Select{ Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), Items: options, @@ -380,7 +381,8 @@ func askForDomain() error { if err != nil { return err } - + //trimmed the '/' from the end of the self hosting url + domain = strings.TrimRight(domain, "/") //set api and login url config.INFISICAL_URL = fmt.Sprintf("%s/api", domain) config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain) diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 7acab93c6..3ff0736f4 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -4,6 +4,21 @@ title: "Changelog" The changelog below reflects new product developments and updates on a monthly basis; it will be updated later this quarter to include issues-addressed on a weekly basis. +## July 2023 + +- Released [secret referencing](https://infisical.com/docs/documentation/platform/secret-reference) across folders and environments. +- Added the [intergation with Laravel Forge](https://infisical.com/docs/integrations/cloud/laravel-forge). +- Redesigned the project/organization experience. + +## June 2023 + +- Released the [Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform#5-run-terraform). +- Updated the usage and billing page. Added the free trial for the professional tier. +- Added the intergation with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages). +- Comleted a penetration test with a `very good` result. +- Added support for multi-line secrets. + + ## May 2023 - Released secret scanning capability for the CLI. @@ -11,8 +26,7 @@ The changelog below reflects new product developments and updates on a monthly b - Completed penetration test. - Released new landing page. - Started SOC 2 (Type II) compliance certification preparation. - -More coming soon. +- Released new deployment options for Fly.io, Digital Ocean and Render. ## April 2023 @@ -107,4 +121,4 @@ More coming soon. - Added search bar to dashboard to query for keys on client-side. - Added capability to rename a project. - Added user roles for projects. -- Added incident contacts. \ No newline at end of file +- Added incident contacts. diff --git a/docs/cli/commands/secrets.mdx b/docs/cli/commands/secrets.mdx index a15829d5f..b3fab541b 100644 --- a/docs/cli/commands/secrets.mdx +++ b/docs/cli/commands/secrets.mdx @@ -99,13 +99,12 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb Default value: `dev` - The `--path` flag indicates which project folder secrets will be injected from. + Used to select the project folder in which the secrets will be set. This is useful when creating new secrets under a particular path. ```bash # Example - infisical secrets set ... --path="/" + infisical secrets set DOMAIN=example.com --path="common/backend" ``` - diff --git a/docs/contributing/overview.mdx b/docs/contributing/overview.mdx index 76475862d..f73422388 100644 --- a/docs/contributing/overview.mdx +++ b/docs/contributing/overview.mdx @@ -30,7 +30,7 @@ If you're ever in doubt about whether or not a proposed feature aligns with Infi ## Writing and submitting code Anyone can contribute code to Infisical. To get started, check out the [local development guide](/contributing/developing), make your changes, and submit a pull request to the main repository -adhering to the [pull request guide](/). +adhering to the [pull request guide](/contributing/pull-requests). ## Licensing diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index 0826a833e..2643ca5af 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -44,10 +44,17 @@ provider "infisical" { ### 3. Fetch Infisical Secrets -Use the `infisical_secrets` data source to fetch your secrets. This is defined with an empty block `{}` as the provider automatically fetches all secrets associated with your service token. +Use the `infisical_secrets` data source to fetch your secrets. In this block, you must set the `env_slug` and `folder_path` to scope the secrets you want. + +`env_slug` is the slug of the environment name. This slug name can be found under the project settings page on the Infisical dashboard. + +`folder_path` is the path to the folder in a given environment. The path `/` for root of the environment where as `/folder1` is the folder at the root of the environment. ```hcl main.tf -data "infisical_secrets" "my-secrets" {} +data "infisical_secrets" "my-secrets" { + env_slug = "dev" + folder_path = "/some-folder/another-folder" +} ``` ### 4. Define Outputs diff --git a/docs/mint.json b/docs/mint.json index 25db15c63..d1175c9e1 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -21,6 +21,10 @@ "to": "#F8B7BD" } }, + "feedback": { + "suggestEdit": true, + "raiseIssue": true + }, "api": { "baseUrl": ["https://app.infisical.com", "http://localhost:8080"], "auth": { diff --git a/docs/self-hosting/configuration/email.mdx b/docs/self-hosting/configuration/email.mdx index 4c6e13a49..15797d73b 100644 --- a/docs/self-hosting/configuration/email.mdx +++ b/docs/self-hosting/configuration/email.mdx @@ -3,12 +3,12 @@ title: "Configure email service" description: "How to configure your email when self-hosting Infisical." --- -By default, the core functions of Infisical work without any email service configuration. Without email service, basic sign up/login and secret operations will function without any issue. +By default, the core functions of Infisical work without any email service configuration. Without email service, basic sign up/login and secret operations will function without any issue. However, the following functionality will be disabled. -- Multi-factor authentication +- Multi-factor authentication - Sending invite links via email for projects to teammates -- Sending alerts such as suspicious login attempts +- Sending alerts such as suspicious login attempts ## General configuration @@ -157,11 +157,30 @@ SMTP_FROM_NAME=Infisical As per the [notice](https://support.google.com/accounts/answer/6010255?hl=en) by Google, you should note that using Gmail credentials for SMTP configuration will only work for Google Workspace or Google Cloud Identity customers as of May 30, 2022. - Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials. +Put differently, the SMTP configuration is only possible with business (not personal) Gmail credentials. + + + +1. Create an account and configure [Office365](https://www.office.com/) to send emails. + +2. With your login credentials, you can now set up your SMTP environment variables: + +``` +SMTP_HOST=smtp.office365.com +SMTP_USERNAME=username@yourdomain.com # your username +SMTP_PASSWORD=password # your password +SMTP_PORT=587 +SMTP_SECURE=true +SMTP_FROM_ADDRESS=username@yourdomain.com +SMTP_FROM_NAME=Infisical +``` + + + 1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails. diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index ef9fc1fd0..fddf6f82e 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -105,7 +105,7 @@ Other environment variables are listed below to increase the functionality of yo - OAuth2 slug for Netlify integration + OAuth2 slug for Vercel integration diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index fbdd12c82..dcc8106f9 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -32,7 +32,8 @@ export default function InitialLoginStep({ const [isLoading, setIsLoading] = useState(false); const [loginError, setLoginError] = useState(false); - const handleLogin = async () => { + const handleLogin = async (e: FormEvent) => { + e.preventDefault() try { if (!email || !password) { return; @@ -101,7 +102,7 @@ export default function InitialLoginStep({ setIsLoading(false); } - return
+ return

Login to Infisical

{/*
-
+ } diff --git a/frontend/src/components/signup/CodeInputStep.tsx b/frontend/src/components/signup/CodeInputStep.tsx index dd765c1fc..7f6c75871 100644 --- a/frontend/src/components/signup/CodeInputStep.tsx +++ b/frontend/src/components/signup/CodeInputStep.tsx @@ -114,6 +114,7 @@ export default function CodeInputStep({
@@ -301,6 +300,7 @@ export default function UserInfoStep({
- , - - - - ]} >

{text}

Upgrade and get access to this, as well as to other powerful enhancements.

+
+ + +
) diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index cffa80d63..847fb4bb9 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -12,5 +12,7 @@ export { useGetOrgPlanTable, useGetOrgPmtMethods, useGetOrgTaxIds, + useGetOrgTrialUrl, useRenameOrg, - useUpdateOrgBillingDetails} from "./queries"; + useUpdateOrgBillingDetails +} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index d3f161f64..3bbb82412 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -10,8 +10,9 @@ import { PlanBillingInfo, PmtMethod, ProductsTable, - RenameOrgDTO, - TaxID} from "./types"; + RenameOrgDTO, + TaxID +} from "./types"; const organizationKeys = { getUserOrganization: ["organization"] as const, @@ -47,6 +48,24 @@ export const useRenameOrg = () => { }); }; +export const useGetOrgTrialUrl = () => { + return useMutation({ + mutationFn: async ({ + orgId, + success_url + }: { + orgId: string; + success_url: string; + }) => { + const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, { + success_url + }) + + return url; + } + }); +}; + export const useGetOrgPlanBillingInfo = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId), diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index ca50255c8..d3d673764 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -15,4 +15,5 @@ export type SubscriptionPlan = { environmentLimit: number; status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; trial_end: number | null; + has_used_trial: boolean; }; diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx index b263a25ff..7c313b0e9 100644 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx @@ -3,7 +3,6 @@ import { Fragment, useMemo } from "react"; import { useTranslation } from "react-i18next"; import Image from "next/image"; -import Link from "next/link"; import { useRouter } from "next/router"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons"; @@ -22,7 +21,9 @@ import {TFunction} from "i18next"; import guidGenerator from "@app/components/utilities/randomId"; import { useOrganization, useSubscription,useUser } from "@app/context"; -import { useLogoutUser } from "@app/hooks/api"; +import { + useGetOrgTrialUrl, + useLogoutUser} from "@app/hooks/api"; const supportOptions = (t: TFunction) => [ [ @@ -67,6 +68,7 @@ export const Navbar = () => { const { subscription } = useSubscription(); const { currentOrg, orgs } = useOrganization(); + const { mutateAsync } = useGetOrgTrialUrl(); const { user } = useUser(); const logout = useLogoutUser(); @@ -96,37 +98,6 @@ export const Navbar = () => { } }; - function formatPlanSlug(slug: string) { - return slug - .replace(/(\b[a-z])/g, match => match.toUpperCase()) - .replace(/-/g, " "); - } - - const calculateRemainingDays = (date: number) => { - const now = new Date(); - const endDate = new Date(date * 1000); - - const differenceInTime = endDate.getTime() - now.getTime(); - const differenceInDays = Math.ceil(differenceInTime / (1000 * 3600 * 24)); - - return differenceInDays; - } - - const formatDate = (date: number) => { - const endDate = new Date(date * 1000); - const day: number = endDate.getDate(); - const month: number = endDate.getMonth() + 1; - const year: number = endDate.getFullYear(); - - const formattedDate: string = `${day}/${month}/${year}`; - const remainingDays: number = calculateRemainingDays(date); - - return { - formattedDate, - remainingDays - }; - } - return (
@@ -346,12 +317,25 @@ export const Navbar = () => {
- {subscription && subscription.status === "trialing" && subscription.trial_end && ( -
-

- {`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `} - Add a card to avoid being downgraded to the Starter plan afterward → -

+ {subscription && subscription.slug === "starter" && !subscription.has_used_trial && ( +
+
)}
diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 4ce6d4004..30efc2c3b 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -1,6 +1,6 @@ import crypto from "crypto"; -import { useState } from "react"; +import { FormEvent, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/router"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; @@ -24,6 +24,7 @@ const client = new jsrp.client(); export default function PasswordReset() { const [verificationToken, setVerificationToken] = useState(""); const [step, setStep] = useState(1); + const [loading, setLoading] = useState(false); const [backupKey, setBackupKey] = useState(""); const [privateKey, setPrivateKey] = useState(""); const [newPassword, setNewPassword] = useState(""); @@ -38,7 +39,8 @@ export default function PasswordReset() { const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); // Unencrypt the private key with a backup key - const getEncryptedKeyHandler = async () => { + const getEncryptedKeyHandler = async (e: FormEvent) => { + e.preventDefault(); try { const result = await getBackupEncryptedPrivateKey({ verificationToken }); @@ -57,7 +59,8 @@ export default function PasswordReset() { }; // If everything is correct, reset the password - const resetPasswordHandler = async () => { + const resetPasswordHandler = async (e: FormEvent) => { + e.preventDefault(); const errorCheck = passwordCheck({ password: newPassword, setPasswordErrorLength, @@ -125,6 +128,7 @@ export default function PasswordReset() { if (response?.status === 200) { router.push("/login"); } + setLoading(false) }); } ); @@ -162,7 +166,7 @@ export default function PasswordReset() { // Input backup key const stepInputBackupKey = ( -
+

Enter your backup key

@@ -186,18 +190,19 @@ export default function PasswordReset() {
-
+ ); // Enter new password const stepEnterNewPassword = ( -
+

Enter new password

@@ -269,13 +274,15 @@ export default function PasswordReset() {
-
+ ); return ( diff --git a/frontend/src/pages/verify-email.tsx b/frontend/src/pages/verify-email.tsx index 33640dbc1..605c71da8 100644 --- a/frontend/src/pages/verify-email.tsx +++ b/frontend/src/pages/verify-email.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { FormEvent, useState } from "react"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; @@ -12,6 +12,7 @@ import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset"; export default function VerifyEmail() { + const [loading, setLoading] = useState(false); const [email, setEmail] = useState(""); const [step, setStep] = useState(1); const { data: serverDetails } = useFetchServerStatus(); @@ -27,6 +28,18 @@ export default function VerifyEmail() { } }; + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + + if (serverDetails?.emailConfigured) { + sendVerificationEmail(); + } else { + handlePopUpOpen("setUpEmail"); + setLoading(false); + } + }; + return (
@@ -45,7 +58,7 @@ export default function VerifyEmail() {
{step === 1 && ( -
+

Forgot your password?

@@ -67,20 +80,10 @@ export default function VerifyEmail() {
-
-
+ )} {step === 2 && (
diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx index 23a4be24d..20791e0e6 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx @@ -2,15 +2,18 @@ import { Button } from "@app/components/v2"; import { useOrganization,useSubscription } from "@app/context"; import { useCreateCustomerPortalSession, - useGetOrgPlanBillingInfo} from "@app/hooks/api"; + useGetOrgPlanBillingInfo, + useGetOrgTrialUrl +} from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; import { ManagePlansModal } from "./ManagePlansModal"; export const PreviewSection = () => { const { currentOrg } = useOrganization(); - const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); + const { subscription } = useSubscription(); const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? ""); + const getOrgTrialUrl = useGetOrgTrialUrl(); const createCustomerPortalSession = useCreateCustomerPortalSession(); const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ @@ -42,19 +45,41 @@ export const PreviewSection = () => { .replace(/-/g, " "); } + const handleUpgradeBtnClick = async () => { + try { + if (!subscription || !currentOrg) return; + + if (!subscription.has_used_trial) { + // direct user to start pro trial + const url = await getOrgTrialUrl.mutateAsync({ + orgId: currentOrg._id, + success_url: window.location.href + }); + + window.location.href = url; + } else { + // open compare plans modal + handlePopUpOpen("managePlan"); + } + } catch (err) { + console.error(err); + } + } + return (
- {!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( + {subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (

Become Infisical

Unlimited members, projects, RBAC, smart alerts, and so much more

)}