Merge pull request #730 from Infisical/main

Catching up the branch
This commit is contained in:
vmatsiiako
2023-07-11 16:19:39 -07:00
committed by GitHub
27 changed files with 307 additions and 118 deletions
@@ -1,10 +1,16 @@
name: Release Docker image for K8 operator name: Release Docker image for K8 operator
on: [workflow_dispatch] on:
push:
tags:
- "infisical-k8-operator/v*.*.*"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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 - uses: actions/checkout@v2
- name: 🔧 Set up QEMU - name: 🔧 Set up QEMU
@@ -26,4 +32,6 @@ jobs:
context: k8-operator context: k8-operator
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: infisical/kubernetes-operator:latest tags: |
infisical/kubernetes-operator:latest
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}
@@ -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 * Return the organization's current plan's billing info
* @param req * @param req
+15
View File
@@ -41,6 +41,21 @@ router.get(
organizationsController.getOrganizationPlan 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( router.get(
"/:organizationId/plan/billing", "/:organizationId/plan/billing",
requireAuth({ requireAuth({
+10 -2
View File
@@ -32,6 +32,7 @@ interface FeatureSet {
auditLogs: boolean; auditLogs: boolean;
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null; status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean;
} }
/** /**
@@ -63,7 +64,8 @@ class EELicenseService {
customAlerts: true, customAlerts: true,
auditLogs: false, auditLogs: false,
status: null, status: null,
trial_end: null trial_end: null,
has_used_trial: true
} }
public localFeatureSet: NodeCache; public localFeatureSet: NodeCache;
@@ -71,7 +73,7 @@ class EELicenseService {
constructor() { constructor() {
this._isLicenseValid = true; this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({ this.localFeatureSet = new NodeCache({
stdTTL: 300, stdTTL: 60,
}); });
} }
@@ -112,6 +114,12 @@ class EELicenseService {
await this.getPlan(organizationId, workspaceId); await this.getPlan(organizationId, workspaceId);
} }
} }
public async delPlan(organizationId: string) {
if (this.instanceType === "cloud") {
this.localFeatureSet.del(`${organizationId}-`);
}
}
public async initGlobalFeatureSet() { public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey(); const licenseServerKey = await getLicenseServerKey();
+13 -3
View File
@@ -2,9 +2,10 @@ import nodemailer from "nodemailer";
import { import {
SMTP_HOST_GMAIL, SMTP_HOST_GMAIL,
SMTP_HOST_MAILGUN, SMTP_HOST_MAILGUN,
SMTP_HOST_OFFICE365,
SMTP_HOST_SENDGRID, SMTP_HOST_SENDGRID,
SMTP_HOST_SOCKETLABS, SMTP_HOST_SOCKETLABS,
SMTP_HOST_ZOHOMAIL, SMTP_HOST_ZOHOMAIL
} from "../variables"; } from "../variables";
import SMTPConnection from "nodemailer/lib/smtp-connection"; import SMTPConnection from "nodemailer/lib/smtp-connection";
import * as Sentry from "@sentry/node"; import * as Sentry from "@sentry/node";
@@ -15,6 +16,7 @@ import {
getSmtpSecure, getSmtpSecure,
getSmtpUsername, getSmtpUsername,
} from "../config"; } from "../config";
import { getLogger } from "../utils/logger";
export const initSmtp = async () => { export const initSmtp = async () => {
const mailOpts: SMTPConnection.Options = { const mailOpts: SMTPConnection.Options = {
@@ -58,6 +60,12 @@ export const initSmtp = async () => {
ciphers: "TLSv1.2", ciphers: "TLSv1.2",
} }
break; break;
case SMTP_HOST_OFFICE365:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: "TLSv1.2"
}
break;
default: default:
if ((await getSmtpHost()).includes("amazonaws.com")) { if ((await getSmtpHost()).includes("amazonaws.com")) {
mailOpts.tls = { mailOpts.tls = {
@@ -73,10 +81,12 @@ export const initSmtp = async () => {
const transporter = nodemailer.createTransport(mailOpts); const transporter = nodemailer.createTransport(mailOpts);
transporter transporter
.verify() .verify()
.then((err) => { .then(async () => {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureMessage("SMTP - Successfully connected"); Sentry.captureMessage("SMTP - Successfully connected");
console.log("SMTP - Successfully connected") (await getLogger("backend-main")).info(
"SMTP - Successfully connected"
);
}) })
.catch(async (err) => { .catch(async (err) => {
Sentry.setUser(null); Sentry.setUser(null);
+1
View File
@@ -3,3 +3,4 @@ export const SMTP_HOST_MAILGUN = "smtp.mailgun.org";
export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com"; export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com";
export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com"; export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com";
export const SMTP_HOST_GMAIL = "smtp.gmail.com"; export const SMTP_HOST_GMAIL = "smtp.gmail.com";
export const SMTP_HOST_OFFICE365 = "smtp.office365.com";
+5 -1
View File
@@ -246,7 +246,11 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req
} }
if response.IsError() { 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 return secretsResponse, nil
+4 -2
View File
@@ -73,7 +73,6 @@ var loginCmd = &cobra.Command{
return return
} }
} }
//override domain //override domain
domainQuery := true domainQuery := true
if config.INFISICAL_URL_MANUAL_OVERRIDE != "" && config.INFISICAL_URL_MANUAL_OVERRIDE != util.INFISICAL_DEFAULT_API_URL { 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} 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{ optionsPrompt := promptui.Select{
Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE),
Items: options, Items: options,
@@ -380,7 +381,8 @@ func askForDomain() error {
if err != nil { if err != nil {
return err return err
} }
//trimmed the '/' from the end of the self hosting url
domain = strings.TrimRight(domain, "/")
//set api and login url //set api and login url
config.INFISICAL_URL = fmt.Sprintf("%s/api", domain) config.INFISICAL_URL = fmt.Sprintf("%s/api", domain)
config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain) config.INFISICAL_LOGIN_URL = fmt.Sprintf("%s/login", domain)
+17 -3
View File
@@ -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. 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 ## May 2023
- Released secret scanning capability for the CLI. - 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. - Completed penetration test.
- Released new landing page. - Released new landing page.
- Started SOC 2 (Type II) compliance certification preparation. - Started SOC 2 (Type II) compliance certification preparation.
- Released new deployment options for Fly.io, Digital Ocean and Render.
More coming soon.
## April 2023 ## April 2023
@@ -107,4 +121,4 @@ More coming soon.
- Added search bar to dashboard to query for keys on client-side. - Added search bar to dashboard to query for keys on client-side.
- Added capability to rename a project. - Added capability to rename a project.
- Added user roles for projects. - Added user roles for projects.
- Added incident contacts. - Added incident contacts.
+2 -3
View File
@@ -99,13 +99,12 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
Default value: `dev` Default value: `dev`
</Accordion> </Accordion>
<Accordion title="--path"> <Accordion title="--path">
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 ```bash
# Example # Example
infisical secrets set <key1=value1> <key2=value2>... --path="/" infisical secrets set DOMAIN=example.com --path="common/backend"
``` ```
</Accordion> </Accordion>
</Accordion> </Accordion>
+1 -1
View File
@@ -30,7 +30,7 @@ If you're ever in doubt about whether or not a proposed feature aligns with Infi
## Writing and submitting code ## 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 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 ## Licensing
+9 -2
View File
@@ -44,10 +44,17 @@ provider "infisical" {
### 3. Fetch Infisical Secrets ### 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 ```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 ### 4. Define Outputs
+4
View File
@@ -21,6 +21,10 @@
"to": "#F8B7BD" "to": "#F8B7BD"
} }
}, },
"feedback": {
"suggestEdit": true,
"raiseIssue": true
},
"api": { "api": {
"baseUrl": ["https://app.infisical.com", "http://localhost:8080"], "baseUrl": ["https://app.infisical.com", "http://localhost:8080"],
"auth": { "auth": {
+23 -4
View File
@@ -3,12 +3,12 @@ title: "Configure email service"
description: "How to configure your email when self-hosting Infisical." 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. However, the following functionality will be disabled.
- Multi-factor authentication - Multi-factor authentication
- Sending invite links via email for projects to teammates - 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 ## 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 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. 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.
</Warning> </Warning>
</Accordion> </Accordion>
<Accordion title="Office365">
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
[email protected] # your username
SMTP_PASSWORD=password # your password
SMTP_PORT=587
SMTP_SECURE=true
[email protected]
SMTP_FROM_NAME=Infisical
```
</Accordion>
<Accordion title="Zoho Mail"> <Accordion title="Zoho Mail">
1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails. 1. Create an account and configure [Zoho Mail](https://www.zoho.com/mail/) to send emails.
+1 -1
View File
@@ -105,7 +105,7 @@ Other environment variables are listed below to increase the functionality of yo
</ParamField> </ParamField>
<ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional> <ParamField query="CLIENT_SLUG_VERCEL" type="string" default="none" optional>
OAuth2 slug for Netlify integration OAuth2 slug for Vercel integration
</ParamField> </ParamField>
</Tab> </Tab>
<Tab title="Auth Integrations"> <Tab title="Auth Integrations">
@@ -1,4 +1,4 @@
import { useState } from "react"; import { FormEvent, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@@ -32,7 +32,8 @@ export default function InitialLoginStep({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [loginError, setLoginError] = useState(false); const [loginError, setLoginError] = useState(false);
const handleLogin = async () => { const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
try { try {
if (!email || !password) { if (!email || !password) {
return; return;
@@ -101,7 +102,7 @@ export default function InitialLoginStep({
setIsLoading(false); setIsLoading(false);
} }
return <div className='flex flex-col mx-auto w-full justify-center items-center'> return <form onSubmit={handleLogin} className='flex flex-col mx-auto w-full justify-center items-center'>
<h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1> <h1 className='text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8' >Login to Infisical</h1>
{/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'> {/* <div className='lg:w-1/6 w-1/4 min-w-[20rem] rounded-md'>
<Button <Button
@@ -146,7 +147,7 @@ export default function InitialLoginStep({
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />} {!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
<div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'> <div className='lg:w-1/6 w-1/4 min-w-[21.2rem] md:min-w-[20.1rem] text-center rounded-md mt-4'>
<Button <Button
onClick={async () => handleLogin()} type="submit"
size="sm" size="sm"
isFullWidth isFullWidth
className='h-12' className='h-12'
@@ -183,5 +184,5 @@ export default function InitialLoginStep({
<span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span> <span className='hover:underline hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer'>Recover your account</span>
</Link> </Link>
</div> </div>
</div> </form>
} }
@@ -114,6 +114,7 @@ export default function CodeInputStep({
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left"> <div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full"> <div className="text-l py-1 text-lg w-full">
<Button <Button
type="submit"
onClick={incrementStep} onClick={incrementStep}
size="sm" size="sm"
isFullWidth isFullWidth
@@ -70,6 +70,7 @@ export default function EnterEmailStep({
<div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left"> <div className="flex flex-col items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full"> <div className="text-l py-1 text-lg w-full">
<Button <Button
type="submit"
onClick={emailCheck} onClick={emailCheck}
size="sm" size="sm"
isFullWidth isFullWidth
@@ -248,7 +248,6 @@ export default function UserInfoStep({
placeholder="" placeholder=""
onChange={(e) => setAttributionSource(e.target.value)} onChange={(e) => setAttributionSource(e.target.value)}
value={attributionSource} value={attributionSource}
isRequired
className="h-12" className="h-12"
/> />
</div> </div>
@@ -301,6 +300,7 @@ export default function UserInfoStep({
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left"> <div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
<div className="text-l py-1 text-lg w-full"> <div className="text-l py-1 text-lg w-full">
<Button <Button
type="submit"
onClick={signupErrorCheck} onClick={signupErrorCheck}
size="sm" size="sm"
isFullWidth isFullWidth
@@ -1,9 +1,10 @@
import Link from "next/link"; import { useOrganization, useSubscription } from "@app/context";
import {
import { useSubscription } from "@app/context"; useGetOrgTrialUrl
} from "@app/hooks/api";
import { Button } from "../Button"; import { Button } from "../Button";
import { Modal, ModalClose, ModalContent } from "../Modal"; import { Modal, ModalContent } from "../Modal";
type Props = { type Props = {
isOpen?: boolean; isOpen?: boolean;
@@ -13,32 +14,61 @@ type Props = {
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => { export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization();
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
const link = (subscription && subscription.slug !== null) const link = (subscription && subscription.slug !== null)
? `/settings/billing/${localStorage.getItem("projectData.id") as string}` ? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
: "https://infisical.com/scheduledemo"; : "https://infisical.com/scheduledemo";
const handleUpgradeBtnClick = async () => {
try {
if (!subscription || !currentOrg) return;
if (!subscription.has_used_trial) {
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
} else {
// direct user to upgrade their plan
window.location.href = link;
}
} catch (err) {
console.error(err);
}
}
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
title="Unleash Infisical's Full Power" title="Unleash Infisical's Full Power"
footerContent={[
<Link
href={link}
key="upgrade-plan"
>
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
</Link>,
<ModalClose asChild key="upgrade-plan-cancel">
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
]}
> >
<p className="mb-2 text-bunker-300">{text}</p> <p className="mb-2 text-bunker-300">{text}</p>
<p className="text-bunker-300"> <p className="text-bunker-300">
Upgrade and get access to this, as well as to other powerful enhancements. Upgrade and get access to this, as well as to other powerful enhancements.
</p> </p>
<div className="mt-8 flex items-center">
<Button
isLoading={isLoading}
colorSchema="primary"
onClick={handleUpgradeBtnClick}
className="mr-4"
>
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => onOpenChange && onOpenChange(false)}
>
Cancel
</Button>
</div>
</ModalContent> </ModalContent>
</Modal> </Modal>
) )
+3 -1
View File
@@ -12,5 +12,7 @@ export {
useGetOrgPlanTable, useGetOrgPlanTable,
useGetOrgPmtMethods, useGetOrgPmtMethods,
useGetOrgTaxIds, useGetOrgTaxIds,
useGetOrgTrialUrl,
useRenameOrg, useRenameOrg,
useUpdateOrgBillingDetails} from "./queries"; useUpdateOrgBillingDetails
} from "./queries";
@@ -10,8 +10,9 @@ import {
PlanBillingInfo, PlanBillingInfo,
PmtMethod, PmtMethod,
ProductsTable, ProductsTable,
RenameOrgDTO, RenameOrgDTO,
TaxID} from "./types"; TaxID
} from "./types";
const organizationKeys = { const organizationKeys = {
getUserOrganization: ["organization"] as const, 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) => { export const useGetOrgPlanBillingInfo = (organizationId: string) => {
return useQuery({ return useQuery({
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId), queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
@@ -15,4 +15,5 @@ export type SubscriptionPlan = {
environmentLimit: number; environmentLimit: number;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean;
}; };
@@ -3,7 +3,6 @@
import { Fragment, useMemo } from "react"; import { Fragment, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons"; import { faGithub, faSlack } from "@fortawesome/free-brands-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-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 guidGenerator from "@app/components/utilities/randomId";
import { useOrganization, useSubscription,useUser } from "@app/context"; import { useOrganization, useSubscription,useUser } from "@app/context";
import { useLogoutUser } from "@app/hooks/api"; import {
useGetOrgTrialUrl,
useLogoutUser} from "@app/hooks/api";
const supportOptions = (t: TFunction) => [ const supportOptions = (t: TFunction) => [
[ [
@@ -67,6 +68,7 @@ export const Navbar = () => {
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg, orgs } = useOrganization(); const { currentOrg, orgs } = useOrganization();
const { mutateAsync } = useGetOrgTrialUrl();
const { user } = useUser(); const { user } = useUser();
const logout = useLogoutUser(); 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 ( return (
<div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white"> <div className="z-[70] border-b border-mineshaft-500 bg-mineshaft-900 text-white">
<div className="flex w-full justify-between px-4"> <div className="flex w-full justify-between px-4">
@@ -346,12 +317,25 @@ export const Navbar = () => {
</Menu> </Menu>
</div> </div>
</div> </div>
{subscription && subscription.status === "trialing" && subscription.trial_end && ( {subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<div className="w-full mx-auto border-t border-mineshaft-500"> <div className="w-full mx-auto border-t border-mineshaft-500 text-center">
<p className="text-center py-4 text-sm"> <button
{`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `} type="button"
<Link href={`/settings/billing/${localStorage.getItem("projectData.id")}`}>Add a card to avoid being downgraded to the Starter plan afterward &rarr;</Link> onClick={async () => {
</p> if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg._id,
success_url: window.location.href
});
window.location.href = url;
}}
className="text-center py-4 text-sm mx-auto"
>
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the full power of Infisical on the <span className="font-semibold">Pro Free Trial &rarr;</span>
</button>
</div> </div>
)} )}
</div> </div>
+16 -9
View File
@@ -1,6 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { useState } from "react"; import { FormEvent, useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
@@ -24,6 +24,7 @@ const client = new jsrp.client();
export default function PasswordReset() { export default function PasswordReset() {
const [verificationToken, setVerificationToken] = useState(""); const [verificationToken, setVerificationToken] = useState("");
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [backupKey, setBackupKey] = useState(""); const [backupKey, setBackupKey] = useState("");
const [privateKey, setPrivateKey] = useState(""); const [privateKey, setPrivateKey] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
@@ -38,7 +39,8 @@ export default function PasswordReset() {
const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
// Unencrypt the private key with a backup key // Unencrypt the private key with a backup key
const getEncryptedKeyHandler = async () => { const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try { try {
const result = await getBackupEncryptedPrivateKey({ verificationToken }); const result = await getBackupEncryptedPrivateKey({ verificationToken });
@@ -57,7 +59,8 @@ export default function PasswordReset() {
}; };
// If everything is correct, reset the password // If everything is correct, reset the password
const resetPasswordHandler = async () => { const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = passwordCheck({ const errorCheck = passwordCheck({
password: newPassword, password: newPassword,
setPasswordErrorLength, setPasswordErrorLength,
@@ -125,6 +128,7 @@ export default function PasswordReset() {
if (response?.status === 200) { if (response?.status === 200) {
router.push("/login"); router.push("/login");
} }
setLoading(false)
}); });
} }
); );
@@ -162,7 +166,7 @@ export default function PasswordReset() {
// Input backup key // Input backup key
const stepInputBackupKey = ( const stepInputBackupKey = (
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6"> <form onSubmit={getEncryptedKeyHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter your backup key Enter your backup key
</p> </p>
@@ -186,18 +190,19 @@ export default function PasswordReset() {
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2"> <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg"> <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button <Button
type="submit"
text="Submit Backup Key" text="Submit Backup Key"
onButtonPressed={() => getEncryptedKeyHandler()} onButtonPressed={() => {}}
size="lg" size="lg"
/> />
</div> </div>
</div> </div>
</div> </form>
); );
// Enter new password // Enter new password
const stepEnterNewPassword = ( const stepEnterNewPassword = (
<div className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6"> <form onSubmit={resetPasswordHandler} className="my-32 mx-1 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pt-6 pb-3 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password Enter new password
</p> </p>
@@ -269,13 +274,15 @@ export default function PasswordReset() {
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2"> <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg"> <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button <Button
type="submit"
text="Submit New Password" text="Submit New Password"
onButtonPressed={() => resetPasswordHandler()} onButtonPressed={() => setLoading(true)}
size="lg" size="lg"
loading={loading}
/> />
</div> </div>
</div> </div>
</div> </form>
); );
return ( return (
+17 -14
View File
@@ -1,4 +1,4 @@
import { useState } from "react"; import { FormEvent, useState } from "react";
import Head from "next/head"; import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
@@ -12,6 +12,7 @@ import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset"; import SendEmailOnPasswordReset from "./api/auth/SendEmailOnPasswordReset";
export default function VerifyEmail() { export default function VerifyEmail() {
const [loading, setLoading] = useState(false);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
@@ -27,6 +28,18 @@ export default function VerifyEmail() {
} }
}; };
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
if (serverDetails?.emailConfigured) {
sendVerificationEmail();
} else {
handlePopUpOpen("setUpEmail");
setLoading(false);
}
};
return ( return (
<div className="flex h-screen flex-col justify-start bg-bunker-800 px-6"> <div className="flex h-screen flex-col justify-start bg-bunker-800 px-6">
<Head> <Head>
@@ -45,7 +58,7 @@ export default function VerifyEmail() {
</div> </div>
</Link> </Link>
{step === 1 && ( {step === 1 && (
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl"> <form onSubmit={onSubmit} className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker px-6 py-4 pt-8 drop-shadow-xl">
<p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl"> <p className="mx-auto mb-6 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Forgot your password? Forgot your password?
</p> </p>
@@ -67,20 +80,10 @@ export default function VerifyEmail() {
</div> </div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2"> <div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg"> <div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button <Button type="submit" text="Continue" size="lg" onButtonPressed={() => {}} loading={loading} />
text="Continue"
onButtonPressed={() => {
if (serverDetails?.emailConfigured) {
sendVerificationEmail();
} else {
handlePopUpOpen("setUpEmail");
}
}}
size="lg"
/>
</div> </div>
</div> </div>
</div> </form>
)} )}
{step === 2 && ( {step === 2 && (
<div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl"> <div className="h-7/12 mx-auto w-full max-w-md rounded-xl bg-bunker py-4 px-6 pt-8 drop-shadow-xl">
@@ -2,15 +2,18 @@ import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context"; import { useOrganization,useSubscription } from "@app/context";
import { import {
useCreateCustomerPortalSession, useCreateCustomerPortalSession,
useGetOrgPlanBillingInfo} from "@app/hooks/api"; useGetOrgPlanBillingInfo,
useGetOrgTrialUrl
} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal"; import { ManagePlansModal } from "./ManagePlansModal";
export const PreviewSection = () => { export const PreviewSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); const { subscription } = useSubscription();
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? ""); const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession(); const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
@@ -42,19 +45,41 @@ export const PreviewSection = () => {
.replace(/-/g, " "); .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 ( return (
<div> <div>
{!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( {subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600 max-w-screen-lg"> <div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mb-6 flex items-center bg-mineshaft-600 max-w-screen-lg">
<div className="flex-1"> <div className="flex-1">
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2> <h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p> <p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
</div> </div>
<Button <Button
onClick={() => handlePopUpOpen("managePlan")} // onClick={() => handlePopUpOpen("managePlan")}
onClick={() => handleUpgradeBtnClick()}
color="mineshaft" color="mineshaft"
> >
Upgrade {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
</Button> </Button>
</div> </div>
)} )}