Merge remote-tracking branch 'origin' into token-auth

This commit is contained in:
Tuan Dang
2024-07-08 17:14:55 +07:00
109 changed files with 6685 additions and 1325 deletions
+2 -3
View File
@@ -4,7 +4,6 @@
"requires": true,
"packages": {
"": {
"name": "frontend",
"dependencies": {
"@casl/ability": "^6.5.0",
"@casl/react": "^3.1.0",
@@ -64,6 +63,7 @@
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"infisical-node": "^1.0.37",
"ip": "^2.0.1",
"jspdf": "^2.5.1",
"jsrp": "^0.2.4",
"jwt-decode": "^3.1.2",
@@ -15767,8 +15767,7 @@
"node_modules/ip": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
"dev": true
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
+1
View File
@@ -71,6 +71,7 @@
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"infisical-node": "^1.0.37",
"ip": "^2.0.1",
"jspdf": "^2.5.1",
"jsrp": "^0.2.4",
"jwt-decode": "^3.1.2",
+1 -1
View File
@@ -10,7 +10,7 @@ export const initPostHog = () => {
try {
if (typeof window !== "undefined") {
// @ts-ignore
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
posthog.init(POSTHOG_API_KEY, {
api_host: POSTHOG_HOST
});
@@ -4,6 +4,9 @@ import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useServerConfig } from "@app/context";
import { LoginMethod } from "@app/hooks/api/admin/types";
import { Button } from "../v2";
export default function InitialSignupStep({
@@ -12,67 +15,79 @@ export default function InitialSignupStep({
setIsSignupWithEmail: (value: boolean) => void;
}) {
const { t } = useTranslation();
const { config } = useServerConfig();
const shouldDisplaySignupMethod = (method: LoginMethod) =>
!config.enabledLoginMethods || config.enabledLoginMethods.includes(method);
return (
<div className="mx-auto flex w-full flex-col items-center justify-center">
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
{t("signup.initial-title")}
</h1>
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-12 w-full"
>
{t("signup.continue-with-google")}
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitHub
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/gitlab");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitLab
</Button>
</div>
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with Email
</Button>
</div>
{shouldDisplaySignupMethod(LoginMethod.GOOGLE) && (
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="solid"
onClick={() => {
window.open("/api/v1/sso/redirect/google");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-12 w-full"
>
{t("signup.continue-with-google")}
</Button>
</div>
)}
{shouldDisplaySignupMethod(LoginMethod.GITHUB) && (
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/github");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitHub
</Button>
</div>
)}
{shouldDisplaySignupMethod(LoginMethod.GITLAB) && (
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
window.open("/api/v1/sso/redirect/gitlab");
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with GitLab
</Button>
</div>
)}
{shouldDisplaySignupMethod(LoginMethod.EMAIL) && (
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setIsSignupWithEmail(true);
}}
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
className="mx-0 h-12 w-full"
>
Continue with Email
</Button>
</div>
)}
<div className="mt-6 w-1/4 min-w-[20rem] px-8 text-center text-xs text-bunker-400 lg:w-1/6">
{t("signup.create-policy")}
</div>
@@ -13,7 +13,7 @@ class Capturer {
}
capture(item: string) {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
try {
this.api.capture(item);
} catch (error) {
@@ -23,7 +23,7 @@ class Capturer {
}
identify(id: string, email?: string) {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === "true") {
if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
try {
this.api.identify(id, {
email: email
+11
View File
@@ -1,3 +1,13 @@
export enum LoginMethod {
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab",
SAML = "saml",
LDAP = "ldap",
OIDC = "oidc"
}
export type TServerConfig = {
initialized: boolean;
allowSignUp: boolean;
@@ -9,6 +19,7 @@ export type TServerConfig = {
isSecretScanningDisabled: boolean;
defaultAuthOrgSlug: string | null;
defaultAuthOrgId: string | null;
enabledLoginMethods: LoginMethod[];
};
export type TCreateAdminUserDTO = {
@@ -362,7 +362,6 @@ interface CreateWebhookEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -373,7 +372,6 @@ interface UpdateWebhookStatusEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -384,7 +382,6 @@ interface DeleteWebhookEvent {
webhookId: string;
environment: string;
secretPath: string;
webhookUrl: string;
isDisabled: boolean;
};
}
@@ -24,7 +24,8 @@ export enum DynamicSecretProviders {
export enum SqlProviders {
Postgres = "postgres",
MySql = "mysql2",
Oracle = "oracledb"
Oracle = "oracledb",
MsSQL = "mssql"
}
export type TDynamicSecretProvider =
@@ -9,12 +9,12 @@ export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath, name }) => {
mutationFn: async ({ environment, workspaceId, approvals, approverUserIds, secretPath, name }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,
approvals,
approvers,
approverUserIds,
secretPath,
name
});
@@ -30,10 +30,10 @@ export const useUpdateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
mutationFn: async ({ id, approvers, approvals, secretPath, name }) => {
mutationFn: async ({ id, approverUserIds, approvals, secretPath, name }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
approvers,
approverUserIds,
secretPath,
name
});
@@ -7,8 +7,8 @@ export type TSecretApprovalPolicy = {
envId: string;
environment: WorkspaceEnv;
secretPath?: string;
approvers: string[];
approvals: number;
userApprovers: { userId: string }[];
};
export type TGetSecretApprovalPoliciesDTO = {
@@ -26,14 +26,14 @@ export type TCreateSecretPolicyDTO = {
name?: string;
environment: string;
secretPath?: string | null;
approvers?: string[];
approverUserIds?: string[];
approvals?: number;
};
export type TUpdateSecretPolicyDTO = {
id: string;
name?: string;
approvers?: string[];
approverUserIds?: string[];
secretPath?: string | null;
approvals?: number;
// for invalidating list
@@ -47,10 +47,14 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
isReplicated?: boolean;
slug: string;
createdAt: string;
committerId: string;
committerUserId: string;
reviewers: {
member: string;
userId: string;
status: ApprovalStatus;
email: string;
firstName: string;
lastName: string;
username: string;
}[];
workspace: string;
environment: string;
@@ -58,8 +62,30 @@ export type TSecretApprovalRequest<J extends unknown = EncryptedSecret> = {
secretPath: string;
hasMerged: boolean;
status: "open" | "close";
policy: TSecretApprovalPolicy;
statusChangeBy: string;
policy: Omit<TSecretApprovalPolicy, "approvers"> & {
approvers: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
}[];
};
statusChangedByUserId: string;
statusChangedByUser?: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
};
committerUser: {
userId: string;
email: string;
firstName: string;
lastName: string;
username: string;
};
conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>;
commits: ({
// if there is no secret means it was creation
@@ -0,0 +1 @@
export { useCreateUserWish } from "./mutations";
@@ -0,0 +1,14 @@
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TCreateUserWishDto } from "./types";
export const useCreateUserWish = () => {
return useMutation<{}, {}, TCreateUserWishDto>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v1/user-engagement/me/wish", dto);
return data;
}
});
};
@@ -0,0 +1,3 @@
export type TCreateUserWishDto = {
text: string;
};
+7
View File
@@ -1,5 +1,11 @@
export enum WebhookType {
GENERAL = "general",
SLACK = "slack"
}
export type TWebhook = {
id: string;
type: WebhookType;
projectId: string;
environment: {
slug: string;
@@ -22,6 +28,7 @@ export type TCreateWebhookDto = {
webhookUrl: string;
webhookSecretKey?: string;
secretPath: string;
type: WebhookType;
};
export type TUpdateWebhookDto = {
+4 -52
View File
@@ -8,7 +8,6 @@
import { useEffect, useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
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";
@@ -69,9 +68,7 @@ import {
useGetAccessRequestsCount,
useGetOrgTrialUrl,
useGetSecretApprovalRequestCount,
useGetUserAction,
useLogoutUser,
useRegisterUserAction,
useSelectOrganization
} from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/types";
@@ -80,6 +77,8 @@ import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
import { navigateUserToOrg } from "@app/views/Login/Login.utils";
import { CreateOrgModal } from "@app/views/Org/components";
import { WishForm } from "./components/WishForm/WishForm";
interface LayoutProps {
children: React.ReactNode;
}
@@ -145,7 +144,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { subscription } = useSubscription();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: updateClosed } = useGetUserAction("december_update_closed");
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
@@ -179,13 +177,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
const { t } = useTranslation();
const registerUserAction = useRegisterUserAction();
const { mutateAsync: selectOrganization } = useSelectOrganization();
const closeUpdate = async () => {
await registerUserAction.mutateAsync("december_update_closed");
};
const logout = useLogoutUser();
const logOutUser = async () => {
try {
@@ -765,49 +758,8 @@ export const AppLayout = ({ children }: LayoutProps) => {
: "mb-4"
} flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400`}
>
{/* <div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[9.9rem] ${router.asPath.includes("org") ? "bottom-[8.4rem]" : "bottom-[5.4rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-30`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[10.7rem] ${router.asPath.includes("org") ? "bottom-[8.15rem]" : "bottom-[5.15rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-50`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[11.5rem] ${router.asPath.includes("org") ? "bottom-[7.9rem]" : "bottom-[4.9rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-70`}/>
<div className={`${isLearningNoteOpen ? "block" : "hidden"} z-0 absolute h-60 w-[12.3rem] ${router.asPath.includes("org") ? "bottom-[7.65rem]" : "bottom-[4.65rem]"} bg-mineshaft-900 border border-mineshaft-600 mb-4 rounded-md opacity-90`}/> */}
<div
className={`${
!updateClosed ? "block" : "hidden"
} relative z-10 mb-6 flex h-64 w-52 flex-col items-center justify-start rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3`}
>
<div className="text-md mt-2 w-full font-semibold text-mineshaft-100">
Infisical December update
</div>
<div className="mt-1 mb-1 w-full text-sm font-normal leading-[1.2rem] text-mineshaft-300">
Infisical Agent, new SDKs, Machine Identities, and more!
</div>
<div className="mt-2 h-[6.77rem] w-full rounded-md border border-mineshaft-700">
<Image
src="/images/infisical-update-december-2023.png"
height={319}
width={539}
alt="kubernetes image"
className="rounded-sm"
/>
</div>
<div className="mt-3 flex w-full items-center justify-between px-0.5">
<button
type="button"
onClick={() => closeUpdate()}
className="text-mineshaft-400 duration-200 hover:text-mineshaft-100"
>
Close
</button>
<a
href="https://infisical.com/blog/infisical-update-december-2023"
target="_blank"
rel="noopener noreferrer"
className="text-sm font-normal leading-[1.2rem] text-mineshaft-400 duration-200 hover:text-mineshaft-100"
>
Learn More{" "}
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="pl-0.5 text-xs" />
</a>
</div>
</div>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
{router.asPath.includes("org") && (
<div
onKeyDown={() => null}
@@ -0,0 +1,109 @@
import { useForm } from "react-hook-form";
import { faRocketchat } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Popover,
PopoverContent,
PopoverTrigger,
TextArea
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useCreateUserWish } from "@app/hooks/api/userEngagement";
const formSchema = z.object({
text: z.string().trim().min(1)
});
type TFormData = z.infer<typeof formSchema>;
export const WishForm = () => {
const {
handleSubmit,
register,
reset,
formState: { isSubmitting, errors }
} = useForm<TFormData>({
resolver: zodResolver(formSchema)
});
const { mutateAsync } = useCreateUserWish();
const [isOpen, setIsOpen] = useToggle(false);
const createWish = async (data: TFormData) => {
try {
await mutateAsync({
text: data.text
});
createNotification({
text: "Your wish has been sent to the Infisical team!",
type: "success"
});
setIsOpen.off();
} catch (err) {
createNotification({
text: "An error occured while sending your wish to the Infisical team.",
type: "error"
});
}
};
return (
<Popover
onOpenChange={() => {
setIsOpen.toggle();
reset();
}}
open={isOpen}
>
<PopoverTrigger asChild>
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
Make a wish
</div>
</PopoverTrigger>
<PopoverContent
hideCloseBtn
align="start"
alignOffset={20}
className="mb-1 w-auto border border-mineshaft-600 bg-mineshaft-900 p-4 drop-shadow-2xl"
sticky="always"
>
<form onSubmit={handleSubmit(createWish)}>
<FormControl
className="mb-0"
isError={Boolean(errors?.text)}
errorText={errors?.text?.message}
>
<TextArea
className="border border-mineshaft-600 bg-black/10 text-sm focus:ring-0"
variant="outline"
placeholder="Wish for anything! Help us improve the platform."
reSize="none"
rows={6}
cols={40}
{...register("text")}
/>
</FormControl>
<div className="flex justify-end pt-2">
<Button
className="w-min"
colorSchema="secondary"
type="submit"
isLoading={isSubmitting}
disabled={isSubmitting}
>
Send
</Button>
</div>
</form>
</PopoverContent>
</Popover>
);
};
@@ -15,6 +15,7 @@ import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useFetchServerStatus } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types";
import { useNavigateToSelectOrganization } from "../../Login.utils";
@@ -61,6 +62,9 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
}
}, []);
const shouldDisplayLoginMethod = (method: LoginMethod) =>
!config.enabledLoginMethods || config.enabledLoginMethods.includes(method);
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
@@ -162,156 +166,179 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
Login to Infisical
</h1>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
{shouldDisplayLoginMethod(LoginMethod.GOOGLE) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-10 w-full"
>
{t("login.continue-with-google")}
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with GitHub
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with GitLab
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
handleSaml(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
router.push("/login/ldap");
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button>
</div>
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
<div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className="w-full border-t border-mineshaft-400/60" />
</div>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-10"
/>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
{shouldShowCaptcha && (
<div className="mt-4">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
window.open(
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
className="mx-0 h-10 w-full"
>
{t("login.continue-with-google")}
</Button>
</div>
)}
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={isLoading}
>
{" "}
Continue with Email{" "}
</Button>
</div>
{shouldDisplayLoginMethod(LoginMethod.GITHUB) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with GitHub
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.GITLAB) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
const callbackPort = queryParams.get("callback_port");
window.open(
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
);
window.close();
}}
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with GitLab
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.SAML) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
handleSaml(2);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with SAML
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.OIDC) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with OIDC
</Button>
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.LDAP) && (
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
router.push("/login/ldap");
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button>
</div>
)}
{(!config.enabledLoginMethods ||
(shouldDisplayLoginMethod(LoginMethod.EMAIL) && config.enabledLoginMethods.length > 1)) && (
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
<div className="w-full border-t border-mineshaft-400/60" />
<span className="mx-2 text-xs text-mineshaft-200">or</span>
<div className="w-full border-t border-mineshaft-400/60" />
</div>
)}
{shouldDisplayLoginMethod(LoginMethod.EMAIL) && (
<>
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="Enter your email..."
isRequired
autoComplete="username"
className="h-10"
/>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
{shouldShowCaptcha && (
<div className="mt-4">
<HCaptcha
theme="dark"
sitekey={CAPTCHA_SITE_KEY}
onVerify={(token) => setCaptchaToken(token)}
ref={captchaRef}
/>
</div>
)}
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
disabled={shouldShowCaptcha && captchaToken === ""}
type="submit"
size="sm"
isFullWidth
className="h-10"
colorSchema="primary"
variant="solid"
isLoading={isLoading}
>
{" "}
Continue with Email{" "}
</Button>
</div>
</>
)}
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
{config.allowSignUp ? (
{config.allowSignUp &&
(shouldDisplayLoginMethod(LoginMethod.EMAIL) ||
shouldDisplayLoginMethod(LoginMethod.GOOGLE) ||
shouldDisplayLoginMethod(LoginMethod.GITHUB) ||
shouldDisplayLoginMethod(LoginMethod.GITLAB)) ? (
<div className="mt-6 flex flex-row text-sm text-bunker-400">
<Link href="/signup">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
@@ -322,13 +349,15 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
) : (
<div className="mt-4" />
)}
<div className="mt-2 flex flex-row text-sm text-bunker-400">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Forgot password? Recover your account
</span>
</Link>
</div>
{shouldDisplayLoginMethod(LoginMethod.EMAIL) && (
<div className="mt-2 flex flex-row text-sm text-bunker-400">
<Link href="/verify-email">
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
Forgot password? Recover your account
</span>
</Link>
</div>
)}
</form>
);
};
@@ -243,7 +243,6 @@ export const LogsTableRow = ({ auditLog }: Props) => {
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
@@ -252,7 +251,6 @@ export const LogsTableRow = ({ auditLog }: Props) => {
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
@@ -261,7 +259,6 @@ export const LogsTableRow = ({ auditLog }: Props) => {
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Secret path: ${event.metadata.secretPath}`}</p>
<p>{`Webhook URL: ${event.metadata.webhookUrl}`}</p>
<p>{`Disabled: ${event.metadata.isDisabled}`}</p>
</Td>
);
@@ -7,6 +7,8 @@ import {
Button,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
@@ -145,13 +147,20 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
</TBody>
</Table>
</TableContainer>
<SecretPolicyForm
workspaceId={workspaceId}
<Modal
isOpen={popUp.secretPolicyForm.isOpen}
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
members={members}
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
/>
onOpenChange={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
>
<ModalContent title={popUp.secretPolicyForm.data ? "Edit policy" : "Create policy"}>
<SecretPolicyForm
workspaceId={workspaceId}
isOpen={popUp.secretPolicyForm.isOpen}
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
members={members}
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
/>
</ModalContent>
</Modal>
<DeleteActionModal
isOpen={popUp.deletePolicy.isOpen}
deleteKey="remove"
@@ -51,7 +51,7 @@ export const SecretApprovalPolicyRow = ({
{
workspaceId,
id: policy.id,
approvers: selectedApprovers
approverUserIds: selectedApprovers
},
{
onSettled: () => {
@@ -60,7 +60,7 @@ export const SecretApprovalPolicyRow = ({
}
);
} else {
setSelectedApprovers(policy.approvers);
setSelectedApprovers(policy.userApprovers.map(({ userId }) => userId));
}
}}
>
@@ -73,7 +73,9 @@ export const SecretApprovalPolicyRow = ({
>
<Input
isReadOnly
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
value={
policy?.userApprovers.length ? `${policy.userApprovers.length} selected` : "None"
}
className="text-left"
/>
</DropdownMenuTrigger>
@@ -84,17 +86,17 @@ export const SecretApprovalPolicyRow = ({
<DropdownMenuLabel>
Select members that are allowed to approve changes
</DropdownMenuLabel>
{members?.map(({ id, user }) => {
const isChecked = selectedApprovers.includes(id);
{members?.map(({ user }) => {
const isChecked = selectedApprovers.includes(user.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
setSelectedApprovers((state) =>
isChecked ? state.filter((el) => el !== id) : [...state, id]
isChecked ? state.filter((el) => el !== user.id) : [...state, user.id]
);
}}
key={`create-policy-members-${id}`}
key={`create-policy-members-${user.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
@@ -1,4 +1,3 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -15,8 +14,6 @@ import {
DropdownMenuTrigger,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
@@ -40,9 +37,9 @@ const formSchema = z
name: z.string().optional(),
secretPath: z.string().optional().nullable(),
approvals: z.number().min(1),
approvers: z.string().array().min(1)
approverUserIds: z.string().array().min(1)
})
.refine((data) => data.approvals <= data.approvers.length, {
.refine((data) => data.approvals <= data.approverUserIds.length, {
path: ["approvals"],
message: "The number of approvals should be lower than the number of approvers."
});
@@ -50,7 +47,6 @@ const formSchema = z
type TFormSchema = z.infer<typeof formSchema>;
export const SecretPolicyForm = ({
isOpen,
onToggle,
members = [],
workspaceId,
@@ -59,20 +55,22 @@ export const SecretPolicyForm = ({
const {
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: editValues ? { ...editValues, environment: editValues.environment.slug } : undefined
values: editValues
? {
...editValues,
approverUserIds: editValues.userApprovers.map(({ userId }) => userId),
environment: editValues.environment.slug
}
: undefined
});
const { currentWorkspace } = useWorkspace();
const selectedEnvironment = watch("environment");
const environments = currentWorkspace?.environments || [];
useEffect(() => {
if (!isOpen) reset({});
}, [isOpen]);
const isEditMode = Boolean(editValues);
@@ -131,8 +129,6 @@ export const SecretPolicyForm = ({
};
return (
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<Controller
control={control}
@@ -155,6 +151,7 @@ export const SecretPolicyForm = ({
errorText={error?.message}
>
<Select
isDisabled={isEditMode}
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
@@ -186,7 +183,7 @@ export const SecretPolicyForm = ({
/>
<Controller
control={control}
name="approvers"
name="approverUserIds"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Approvers Required"
@@ -208,17 +205,19 @@ export const SecretPolicyForm = ({
<DropdownMenuLabel>
Select members that are allowed to approve changes
</DropdownMenuLabel>
{members.map(({ id, user }) => {
const isChecked = value?.includes(id);
{members.map(({ user }) => {
const isChecked = value?.includes(user.id);
return (
<DropdownMenuItem
onClick={(evt) => {
evt.preventDefault();
onChange(
isChecked ? value?.filter((el) => el !== id) : [...(value || []), id]
isChecked
? value?.filter((el) => el !== user.id)
: [...(value || []), user.id]
);
}}
key={`create-policy-members-${id}`}
key={`create-policy-members-${user.id}`}
iconPos="right"
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
>
@@ -258,7 +257,6 @@ export const SecretPolicyForm = ({
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -31,7 +31,7 @@ import {
useGetSecretApprovalRequests,
useGetWorkspaceUsers
} from "@app/hooks/api";
import { ApprovalStatus, TSecretApprovalRequest, TWorkspaceUser } from "@app/hooks/api/types";
import { ApprovalStatus, TSecretApprovalRequest } from "@app/hooks/api/types";
import {
generateCommitText,
@@ -63,14 +63,9 @@ export const SecretApprovalRequest = () => {
});
const { data: secretApprovalRequestCount, isSuccess: isSecretApprovalReqCountSuccess } =
useGetSecretApprovalRequestCount({ workspaceId });
const { user: presentUser } = useUser();
const { user: userSession } = useUser();
const { permission } = useProjectPermission();
const { data: members } = useGetWorkspaceUsers(workspaceId);
const membersGroupById = members?.reduce<Record<string, TWorkspaceUser>>(
(prev, curr) => ({ ...prev, [curr.id]: curr }),
{}
);
const myMembershipId = members?.find(({ user }) => user.id === presentUser?.id)?.id;
const isSecretApprovalScreen = Boolean(selectedApproval);
const handleGoBackSecretRequestDetail = () => {
@@ -93,10 +88,8 @@ export const SecretApprovalRequest = () => {
>
<SecretApprovalRequestChanges
workspaceId={workspaceId}
members={membersGroupById}
approvalRequestId={selectedApproval?.id || ""}
onGoBack={handleGoBackSecretRequestDetail}
committer={membersGroupById?.[selectedApproval?.committerId || ""]}
/>
</motion.div>
) : (
@@ -182,10 +175,12 @@ export const SecretApprovalRequest = () => {
{members?.map(({ user, id }) => (
<DropdownMenuItem
onClick={() =>
setCommitterFilter((state) => (state === id ? undefined : id))
setCommitterFilter((state) => (state === user.id ? undefined : user.id))
}
key={`request-filter-member-${id}`}
icon={committerFilter === id && <FontAwesomeIcon icon={faCheckCircle} />}
icon={
committerFilter === user.id && <FontAwesomeIcon icon={faCheckCircle} />
}
iconPos="right"
>
{user.username}
@@ -208,19 +203,16 @@ export const SecretApprovalRequest = () => {
const {
id: reqId,
commits,
committerId,
createdAt,
policy,
reviewers,
status,
committerUser,
isReplicated: isReplication
} = secretApproval;
const isApprover = policy?.approvers?.indexOf(myMembershipId || "") !== -1;
const isReviewed =
reviewers.findIndex(
({ member, status: reviewStatus }) =>
member === myMembershipId && reviewStatus === ApprovalStatus.APPROVED
) !== -1;
const isReviewed = reviewers.some(
({ status: reviewStatus, userId }) =>
userId === userSession.id && reviewStatus === ApprovalStatus.APPROVED
);
return (
<div
key={reqId}
@@ -239,11 +231,9 @@ export const SecretApprovalRequest = () => {
</div>
<span className="text-xs text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
{membersGroupById?.[committerId]?.user?.firstName}{" "}
{membersGroupById?.[committerId]?.user?.lastName} (
{membersGroupById?.[committerId]?.user?.email})
{isReplication && " via replication"}
{isApprover && !isReviewed && status === "open" && " - Review required"}
{committerUser?.firstName || ""} {committerUser?.lastName || ""} (
{committerUser?.email}){isReplication && " via replication"}
{!isReviewed && status === "open" && " - Review required"}
</span>
</div>
);
@@ -23,7 +23,7 @@ type Props = {
status: "close" | "open";
approvals: number;
canApprove?: boolean;
statusChangeByEmail: string;
statusChangeByEmail?: string;
workspaceId: string;
};
@@ -19,7 +19,7 @@ import {
useGetUserWsKey,
useUpdateSecretApprovalReviewStatus
} from "@app/hooks/api";
import { ApprovalStatus, CommitType, TWorkspaceUser } from "@app/hooks/api/types";
import { ApprovalStatus, CommitType } from "@app/hooks/api/types";
import { formatReservedPaths } from "@app/lib/fn/string";
import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction";
@@ -73,18 +73,14 @@ type Props = {
workspaceId: string;
approvalRequestId: string;
onGoBack: () => void;
committer?: TWorkspaceUser;
members?: Record<string, TWorkspaceUser>;
};
export const SecretApprovalRequestChanges = ({
approvalRequestId,
onGoBack,
committer,
workspaceId,
members = {}
workspaceId
}: Props) => {
const { user } = useUser();
const { user: userSession } = useUser();
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const {
data: secretApprovalRequestDetails,
@@ -105,22 +101,20 @@ export const SecretApprovalRequestChanges = ({
const isRejecting = variables?.status === ApprovalStatus.REJECTED && isUpdatingRequestStatus;
// membership of present user
const myMembership = Object.values(members).find(
({ user: membershipUser }) => membershipUser.email === user.email
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.some(
({ userId }) => userId === userSession.id
);
const myMembershipId = myMembership?.id || "";
const canApprove = secretApprovalRequestDetails?.policy?.approvers?.includes(myMembershipId);
const reviewedMembers = secretApprovalRequestDetails?.reviewers?.reduce<
const reviewedUsers = secretApprovalRequestDetails?.reviewers?.reduce<
Record<string, ApprovalStatus>
>(
(prev, curr) => ({
...prev,
[curr.member]: curr.status
[curr.userId]: curr.status
}),
{}
);
const hasApproved = reviewedMembers?.[myMembershipId] === ApprovalStatus.APPROVED;
const hasRejected = reviewedMembers?.[myMembershipId] === ApprovalStatus.REJECTED;
const hasApproved = reviewedUsers?.[userSession.id] === ApprovalStatus.APPROVED;
const hasRejected = reviewedUsers?.[userSession.id] === ApprovalStatus.REJECTED;
const handleSecretApprovalStatusUpdate = async (status: ApprovalStatus) => {
try {
@@ -159,7 +153,7 @@ export const SecretApprovalRequestChanges = ({
const isMergable =
secretApprovalRequestDetails?.policy?.approvals <=
secretApprovalRequestDetails?.policy?.approvers?.filter(
(approverId) => reviewedMembers?.[approverId] === ApprovalStatus.APPROVED
({ userId }) => reviewedUsers?.[userId] === ApprovalStatus.APPROVED
).length;
const hasMerged = secretApprovalRequestDetails?.hasMerged;
@@ -191,8 +185,9 @@ export const SecretApprovalRequestChanges = ({
)}
</div>
<div className="flex items-center text-sm text-bunker-300">
{committer?.user?.firstName}
{committer?.user?.lastName} ({committer?.user?.email}) wants to change{" "}
{secretApprovalRequestDetails?.committerUser?.firstName || ""}
{secretApprovalRequestDetails?.committerUser?.lastName || ""} (
{secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "}
{secretApprovalRequestDetails.commits.length} secret values in
<span className="mx-1 rounded bg-primary-600/60 px-1 text-primary-300">
{secretApprovalRequestDetails.environment}
@@ -256,9 +251,7 @@ export const SecretApprovalRequestChanges = ({
approvals={secretApprovalRequestDetails.policy.approvals || 0}
status={secretApprovalRequestDetails.status}
isMergable={isMergable}
statusChangeByEmail={
members[secretApprovalRequestDetails?.statusChangeBy || ""]?.user?.email || ""
}
statusChangeByEmail={secretApprovalRequestDetails.statusChangedByUser?.email}
workspaceId={workspaceId}
/>
</div>
@@ -266,17 +259,19 @@ export const SecretApprovalRequestChanges = ({
<div className="sticky top-0 w-1/5 pt-4" style={{ minWidth: "240px" }}>
<div className="text-sm text-bunker-300">Reviewers</div>
<div className="mt-2 flex flex-col space-y-2 text-sm">
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApproverId) => {
const userDetails = members?.[requiredApproverId]?.user;
const status = reviewedMembers?.[requiredApproverId];
{secretApprovalRequestDetails?.policy?.approvers.map((requiredApprover) => {
const status = reviewedUsers?.[requiredApprover.userId];
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${requiredApproverId}`}
key={`required-approver-${requiredApprover.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
<span>{userDetails?.email} </span>
<Tooltip
content={`${requiredApprover.firstName || ""} ${requiredApprover.lastName || ""
}`}
>
<span>{requiredApprover?.email} </span>
</Tooltip>
<span className="text-red">*</span>
</div>
@@ -290,19 +285,21 @@ export const SecretApprovalRequestChanges = ({
})}
{secretApprovalRequestDetails?.reviewers
.filter(
({ member }) => !secretApprovalRequestDetails?.policy?.approvers?.includes(member)
(reviewer) =>
!secretApprovalRequestDetails?.policy?.approvers?.some(
({ userId }) => userId === reviewer.userId
)
)
.map((reviewer) => {
const userDetails = members?.[reviewer.member]?.user;
const status = reviewedMembers?.[reviewer.status];
const status = reviewedUsers?.[reviewer.userId];
return (
<div
className="flex flex-nowrap items-center space-x-2 rounded bg-mineshaft-800 px-2 py-1"
key={`required-approver-${reviewer.member}`}
key={`required-approver-${reviewer.userId}`}
>
<div className="flex-grow text-sm">
<Tooltip content={`${userDetails.firstName} ${userDetails.lastName}`}>
<span>{userDetails?.email} </span>
<Tooltip content={`${reviewer.firstName || ""} ${reviewer.lastName || ""}`}>
<span>{reviewer?.email} </span>
</Tooltip>
<span className="text-red">*</span>
</div>
@@ -53,13 +53,12 @@ export const SecretMainPage = () => {
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
const router = useRouter();
const { permission } = useProjectPermission();
const [isVisible, setIsVisible] = useState(false);
const [sortDir, setSortDir] = useState<SortDir>(SortDir.ASC);
const [filter, setFilter] = useState<Filter>({
tags: {},
searchFilter: ""
searchFilter: (router.query.searchFilter as string) || ""
});
const [snapshotId, setSnapshotId] = useState<string | null>(null);
@@ -334,7 +334,7 @@ export const ActionBar = ({
className="h-10"
isDisabled={!isAllowed}
>
{`${snapshotCount} ${snapshotCount === 1 ? "Commit" : "Commits"}`}
{`${snapshotCount} ${snapshotCount === 1 ? "Snapshot" : "Snapshots"}`}
</Button>
)}
</ProjectPermissionCan>
@@ -86,6 +86,14 @@ const getSqlStatements = (provider: SqlProviders) => {
'REVOKE CONNECT FROM "{{username}}";\nREVOKE CREATE SESSION FROM "{{username}}";\nDROP USER "{{username}}";'
};
}
if (provider === SqlProviders.MsSQL) {
return {
creationStatement:
"CREATE LOGIN [{{username}}] WITH PASSWORD = '{{password}}';\nCREATE USER [{{username}}] FOR LOGIN [{{username}}];\nGRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA::dbo TO [{{username}}];",
renewStatement: "",
revocationStatement: "DROP USER [{{username}}];\nDROP LOGIN [{{username}}];"
};
}
return {
creationStatement:
@@ -96,6 +104,19 @@ const getSqlStatements = (provider: SqlProviders) => {
};
};
const getDefaultPort = (provider: SqlProviders) => {
switch (provider) {
case SqlProviders.MySql:
return 3306;
case SqlProviders.Oracle:
return 1521;
case SqlProviders.MsSQL:
return 1433;
default:
return 5432;
}
};
export const SqlDatabaseInputForm = ({
onCompleted,
onCancel,
@@ -139,6 +160,14 @@ export const SqlDatabaseInputForm = ({
}
};
const handleDatabaseChange = (type: SqlProviders) => {
const sqlStatment = getSqlStatements(type);
setValue("provider.creationStatement", sqlStatment.creationStatement);
setValue("provider.renewStatement", sqlStatment.renewStatement);
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
setValue("provider.port", getDefaultPort(type));
};
return (
<div>
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
@@ -155,7 +184,7 @@ export const SqlDatabaseInputForm = ({
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="dynamic-postgres" />
<Input {...field} placeholder="dynamic-secret" />
</FormControl>
)}
/>
@@ -209,16 +238,14 @@ export const SqlDatabaseInputForm = ({
value={value}
onValueChange={(val) => {
onChange(val);
const sqlStatment = getSqlStatements(val as SqlProviders);
setValue("provider.creationStatement", sqlStatment.creationStatement);
setValue("provider.renewStatement", sqlStatment.renewStatement);
setValue("provider.revocationStatement", sqlStatment.revocationStatement);
handleDatabaseChange(val as SqlProviders);
}}
className="w-full border border-mineshaft-500"
>
<SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem>
<SelectItem value={SqlProviders.MySql}>MySQL</SelectItem>
<SelectItem value={SqlProviders.Oracle}>Oracle</SelectItem>
<SelectItem value={SqlProviders.MsSQL}>MS SQL</SelectItem>
</Select>
</FormControl>
)}
@@ -194,6 +194,7 @@ export const EditDynamicSecretSqlProviderForm = ({
<SelectItem value={SqlProviders.Postgres}>PostgreSQL</SelectItem>
<SelectItem value={SqlProviders.MySql}>MySQL</SelectItem>
<SelectItem value={SqlProviders.Oracle}>Oracle</SelectItem>
<SelectItem value={SqlProviders.MsSQL}>MS SQL</SelectItem>
</Select>
</FormControl>
)}
@@ -250,7 +250,7 @@ export const SecretItem = memo(
/>
</div>
<div
className="flex flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
tabIndex={0}
role="button"
>
@@ -498,7 +498,7 @@ export const SecretItem = memo(
{!isDirty ? (
<motion.div
key="options"
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
initial={{ x: 0, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }}
@@ -425,7 +425,8 @@ export const SecretOverviewPage = () => {
});
}
}
const query: Record<string, string> = { ...router.query, env: slug };
const query: Record<string, string> = { ...router.query, env: slug, searchFilter };
const envIndex = visibleEnvs.findIndex((el) => slug === el.slug);
if (envIndex !== -1) {
router.push({
@@ -1,5 +1,6 @@
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useServerConfig } from "@app/context";
import { withPermission } from "@app/hoc";
import { LoginMethod } from "@app/hooks/api/admin/types";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgLDAPSection } from "./OrgLDAPSection";
@@ -9,12 +10,23 @@ import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = withPermission(
() => {
const {
config: { enabledLoginMethods }
} = useServerConfig();
const shouldDisplaySection = (method: LoginMethod) =>
!enabledLoginMethods || enabledLoginMethods.includes(method);
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<OrgGeneralAuthSection />
<OrgSSOSection />
<OrgOIDCSection />
<OrgLDAPSection />
{shouldDisplaySection(LoginMethod.SAML) && (
<>
<OrgGeneralAuthSection />
<OrgSSOSection />
</>
)}
{shouldDisplaySection(LoginMethod.OIDC) && <OrgOIDCSection />}
{shouldDisplaySection(LoginMethod.LDAP) && <OrgLDAPSection />}
<OrgScimSection />
</div>
);
@@ -11,7 +11,6 @@ import { useLogoutUser, useUpdateOrg } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
export const OrgGeneralAuthSection = () => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
@@ -88,6 +87,7 @@ export const OrgGeneralAuthSection = () => {
Enforce members to authenticate via SAML to access this organization
</p>
</div>
<hr className="border-mineshaft-600" />
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@@ -95,7 +95,6 @@ export const OrgLDAPSection = (): JSX.Element => {
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">LDAP</h2>
@@ -152,6 +151,7 @@ export const OrgLDAPSection = (): JSX.Element => {
</p>
</div>
)}
<hr className="border-mineshaft-600" />
<LDAPModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -61,7 +61,6 @@ export const OrgOIDCSection = (): JSX.Element => {
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">OIDC</h2>
@@ -103,6 +102,7 @@ export const OrgOIDCSection = (): JSX.Element => {
</p>
</div>
)}
<hr className="border-mineshaft-600" />
<OIDCModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -13,7 +13,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { ScimTokenModal } from "./ScimTokenModal";
export const OrgScimSection = () => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
@@ -59,7 +58,6 @@ export const OrgScimSection = () => {
return (
<>
<hr className="border-mineshaft-600" />
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">SCIM</h2>
@@ -15,7 +15,7 @@ import { SSOModal } from "./SSOModal";
export const OrgSSOSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { subscription } = useSubscription();
const { data, isLoading } = useGetSSOConfig(currentOrg?.id ?? "");
const { mutateAsync } = useUpdateSSOConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
@@ -115,6 +115,7 @@ export const OrgSSOSection = (): JSX.Element => {
Allow members to authenticate into Infisical with SAML
</p>
</div>
<hr className="border-mineshaft-600" />
<SSOModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
@@ -8,23 +8,24 @@ import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { Switch } from "@app/components/v2";
import { useUser } from "@app/context";
import { useServerConfig, useUser } from "@app/context";
import { useUpdateUserAuthMethods } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types";
import { AuthMethod } from "@app/hooks/api/users/types";
interface AuthMethodOption {
label: string;
value: AuthMethod;
icon: IconDefinition;
loginMethod: LoginMethod;
}
const authMethodOpts: AuthMethodOption[] = [
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub },
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab }
{ label: "Email", value: AuthMethod.EMAIL, icon: faEnvelope, loginMethod: LoginMethod.EMAIL },
{ label: "Google", value: AuthMethod.GOOGLE, icon: faGoogle, loginMethod: LoginMethod.GOOGLE },
{ label: "GitHub", value: AuthMethod.GITHUB, icon: faGithub, loginMethod: LoginMethod.GITHUB },
{ label: "GitLab", value: AuthMethod.GITLAB, icon: faGitlab, loginMethod: LoginMethod.GITLAB }
];
const schema = yup.object({
authMethods: yup.array().required("Auth method is required")
});
@@ -32,8 +33,8 @@ const schema = yup.object({
export type FormData = yup.InferType<typeof schema>;
export const AuthMethodSection = () => {
const { user } = useUser();
const { config } = useServerConfig();
const { mutateAsync } = useUpdateUserAuthMethods();
const { reset, setValue, watch } = useForm<FormData>({
@@ -102,6 +103,14 @@ export const AuthMethodSection = () => {
<div className="mb-4">
{user &&
authMethodOpts.map((authMethodOpt) => {
// only filter when enabledLoginMethods is explicitly configured by admin
if (
config.enabledLoginMethods &&
!config.enabledLoginMethods.includes(authMethodOpt.loginMethod)
) {
return null;
}
return (
<div className="flex items-center p-4" key={`auth-method-${authMethodOpt.value}`}>
<div className="flex items-center">
@@ -1,9 +1,8 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { zodResolver } from "@hookform/resolvers/zod";
import z from "zod";
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
import {
Button,
FormControl,
@@ -14,15 +13,28 @@ import {
Select,
SelectItem
} from "@app/components/v2";
import { SecretPathInput } from "@app/components/v2/SecretPathInput";
import { WebhookType } from "@app/hooks/api/webhooks/types";
const formSchema = yup.object({
environment: yup.string().required().trim().label("Environment"),
webhookUrl: yup.string().url().required().trim().label("Webhook URL"),
webhookSecretKey: yup.string().trim().label("Secret Key"),
secretPath: yup.string().required().trim().label("Secret Path")
});
const formSchema = z
.object({
environment: z.string().trim().describe("Environment"),
webhookUrl: z.string().url().trim().describe("Webhook URL"),
webhookSecretKey: z.string().trim().optional().describe("Secret Key"),
secretPath: z.string().trim().describe("Secret Path"),
type: z.nativeEnum(WebhookType).describe("Type").default(WebhookType.GENERAL)
})
.superRefine((data, ctx) => {
if (data.type === WebhookType.SLACK && !data.webhookUrl.includes("hooks.slack.com")) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Incoming Webhook URL is invalid.",
path: ["webhookUrl"]
});
}
});
export type TFormSchema = yup.InferType<typeof formSchema>;
export type TFormSchema = z.infer<typeof formSchema>;
type Props = {
isOpen: boolean;
@@ -42,11 +54,50 @@ export const AddWebhookForm = ({
handleSubmit,
register,
reset,
watch,
formState: { errors, isSubmitting }
} = useForm<TFormSchema>({
resolver: yupResolver(formSchema)
resolver: zodResolver(formSchema),
defaultValues: {
type: WebhookType.GENERAL
}
});
const selectedWebhookType = watch("type");
const selectedEnvironment = watch("environment");
const generalFormFields = (
<>
<FormControl
label="Secret Key"
isError={Boolean(errors?.webhookSecretKey)}
errorText={errors?.webhookSecretKey?.message}
helperText="To generate webhook signature for verification"
>
<Input placeholder="Provided during webhook setup" {...register("webhookSecretKey")} />
</FormControl>
<FormControl
label="Webhook URL"
isRequired
isError={Boolean(errors?.webhookUrl)}
errorText={errors?.webhookUrl?.message}
>
<Input {...register("webhookUrl")} />
</FormControl>
</>
);
const slackFormFields = (
<FormControl
label="Incoming Webhook URL"
isRequired
isError={Boolean(errors?.webhookUrl)}
errorText={errors?.webhookUrl?.message}
>
<Input placeholder="https://hooks.slack.com/services/..." {...register("webhookUrl")} />
</FormControl>
);
useEffect(() => {
if (!isOpen) {
reset();
@@ -58,6 +109,32 @@ export const AddWebhookForm = ({
<ModalContent title="Create a new webhook">
<form onSubmit={handleSubmit(onCreateWebhook)}>
<div>
<Controller
control={control}
name="type"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Type"
isRequired
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
<SelectItem value={WebhookType.GENERAL} key={WebhookType.GENERAL}>
General
</SelectItem>
<SelectItem value={WebhookType.SLACK} key={WebhookType.SLACK}>
Slack
</SelectItem>
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="environment"
@@ -84,38 +161,22 @@ export const AddWebhookForm = ({
</FormControl>
)}
/>
<FormControl
label="Secret Path"
icon={<GlobPatternExamples />}
isRequired
isError={Boolean(errors?.secretPath)}
errorText={errors?.secretPath?.message}
helperText="Glob patterns are used to match multiple files or directories"
>
<Input
placeholder="glob pattern / or /**/* or /{dir1,dir2}"
{...register("secretPath")}
/>
</FormControl>
<FormControl
label="Secret Key"
isError={Boolean(errors?.webhookSecretKey)}
errorText={errors?.webhookSecretKey?.message}
helperText="To generate webhook signature for verification"
>
<Input
placeholder="Provided during webhook setup"
{...register("webhookSecretKey")}
/>
</FormControl>
<FormControl
label="Webhook URL"
isRequired
isError={Boolean(errors?.webhookUrl)}
errorText={errors?.webhookUrl?.message}
>
<Input {...register("webhookUrl")} />
</FormControl>
<Controller
control={control}
defaultValue=""
name="secretPath"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Secret Path"
isRequired
isError={Boolean(error)}
errorText={error?.message}
>
<SecretPathInput {...field} environment={selectedEnvironment} placeholder="/" />
</FormControl>
)}
/>
{selectedWebhookType === WebhookType.SLACK ? slackFormFields : generalFormFields}
</div>
<div className="mt-8 flex items-center">
<Button
@@ -6,12 +6,12 @@ import * as yup from "yup";
import { createNotification } from "@app/components/notifications";
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { Button, Checkbox, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"),
expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
expiresAfterSingleView: yup.boolean().required().label("Expires After Views"),
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
expiresInUnit: yup.string().required().label("Expiration Unit")
});
@@ -65,7 +65,7 @@ export const AddShareSecretForm = ({
value,
expiresInValue,
expiresInUnit,
expiresAfterViews
expiresAfterSingleView
}: FormData) => {
try {
const key = crypto.randomBytes(16).toString("hex");
@@ -89,7 +89,7 @@ export const AddShareSecretForm = ({
tag,
hashedHex,
expiresAt,
expiresAfterViews
expiresAfterViews: expiresAfterSingleView ? 1 : 1000
});
setNewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
@@ -118,11 +118,11 @@ export const AddShareSecretForm = ({
}
};
return (
<form className="flex w-full flex-col items-center" onSubmit={handleSubmit(onFormSubmit)}>
<form className="flex w-full flex-col items-center px-4 sm:px-0" onSubmit={handleSubmit(onFormSubmit)}>
<div
className={`${!inModal && "rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"}`}
className={`w-full ${!inModal && "rounded-md border border-mineshaft-600 bg-mineshaft-800 p-6"}`}
>
<div className="mb-4">
<div>
<Controller
control={control}
name="value"
@@ -131,6 +131,7 @@ export const AddShareSecretForm = ({
label="Shared Secret"
isError={Boolean(error)}
errorText={error?.message}
className="mb-2"
>
<textarea
disabled={isInputDisabled}
@@ -142,39 +143,20 @@ export const AddShareSecretForm = ({
)}
/>
</div>
<div className="flex w-full flex-row justify-center">
<div className="flex hidden sm:block sm:w-2/6">
<Controller
control={control}
name="expiresAfterViews"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4 w-full"
label="Expires after Views"
isError={Boolean(error)}
errorText="Please enter a valid number of views"
>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
</div>
<div className="sm:w-1/7 mx-auto hidden items-center justify-center px-2 sm:flex">
<p className="px-4 text-sm text-gray-400">OR</p>
</div>
<div className="flex w-full justify-end sm:w-3/6">
<div className="flex w-full flex-col md:flex-row justify-start">
<div className="flex justify-start">
<div className="flex justify-start">
<div className="flex w-full justify-center pr-2">
<Controller
control={control}
name="expiresInValue"
defaultValue={10}
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expires after Time"
isError={Boolean(error)}
errorText="Please enter a valid time duration"
className="w-32"
>
<Input {...field} type="number" min={0} />
</FormControl>
@@ -185,7 +167,7 @@ export const AddShareSecretForm = ({
<Controller
control={control}
name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit}
defaultValue={expirationUnitsAndActions[1].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Unit" errorText={error?.message} isError={Boolean(error)}>
<Select
@@ -206,9 +188,47 @@ export const AddShareSecretForm = ({
</div>
</div>
</div>
<div className="sm:w-1/7 mx-auto items-center justify-center px-6 hidden md:flex">
<p className="px-4 mt-2 text-sm text-gray-400">AND</p>
</div>
<div className="items-center pb-4 md:pb-0 md:pt-2 flex">
<Controller
control={control}
name="expiresAfterViews"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4 w-full hidden"
label="Expires after Views"
isError={Boolean(error)}
errorText="Please enter a valid number of views"
>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
<div className="bg-mineshaft-900 py-2 h-max rounded-md border border-mineshaft-600 px-4">
<Controller
control={control}
name="expiresAfterSingleView"
defaultValue={false}
render={({ field: { onBlur, value, onChange } }) => (
<Checkbox
id="is-single-view"
isChecked={value}
onCheckedChange={onChange}
isDisabled={false}
onBlur={onBlur}
>
Can be viewed only 1 time
</Checkbox>
)}
/>
</div>
</div>
</div>
<div className={`flex items-center ${!inModal && "justify-left pt-2"}`}>
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
<div className={`flex items-center ${!inModal && "justify-start pt-2"}`}>
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
{inModal ? "Create" : "Share Secret"}
</Button>
{inModal && (
@@ -125,7 +125,7 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
<div className="m-auto my-6 flex w-full max-w-xl justify-center px-8 px-4 sm:my-8">
<div className="w-full border-t border-mineshaft-600" />
</div>
<div className="flex flex-col justify-center items-center m-auto max-w-2xl px-6">
<div className="flex flex-col justify-center items-center m-auto max-w-2xl px-4 sm:px-6">
<div className="m-auto mb-12 flex flex max-w-2xl w-full flex-col justify-center rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-3 text-lg md:text-xl w-full">
Open source <span className="bg-clip-text text-transparent bg-gradient-to-tr from-yellow-500 to-primary-500">secret management</span> for developers
@@ -26,7 +26,7 @@ export const SecretTable = ({
</Tr>
)}
{!isLoading && decryptedSecret && (
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 p-2">
<div className="dark relative flex h-full w-full items-center overflow-y-auto border border-mineshaft-700 bg-mineshaft-900 rounded-md p-2 md:p-3 pr-2">
<div className="thin-scrollbar flex h-full max-h-44 w-full flex-1 overflow-y-scroll break-words pr-4 dark:[color-scheme:dark]">
<div className="align-center flex w-full min-w-full whitespace-pre-line">
{decryptedSecret}
@@ -0,0 +1,252 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Switch } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useUpdateServerConfig } from "@app/hooks/api";
import { LoginMethod } from "@app/hooks/api/admin/types";
const formSchema = z.object({
isEmailEnabled: z.boolean(),
isGoogleEnabled: z.boolean(),
isGithubEnabled: z.boolean(),
isGitlabEnabled: z.boolean(),
isSamlEnabled: z.boolean(),
isLdapEnabled: z.boolean(),
isOidcEnabled: z.boolean()
});
type TAuthForm = z.infer<typeof formSchema>;
export const AuthPanel = () => {
const { config } = useServerConfig();
const { enabledLoginMethods } = config;
const { mutateAsync: updateServerConfig } = useUpdateServerConfig();
const {
control,
handleSubmit,
formState: { isSubmitting, isDirty }
} = useForm<TAuthForm>({
resolver: zodResolver(formSchema),
// if not yet explicitly defined by the admin, all login methods should be enabled by default
values: enabledLoginMethods
? {
isEmailEnabled: enabledLoginMethods.includes(LoginMethod.EMAIL),
isGoogleEnabled: enabledLoginMethods.includes(LoginMethod.GOOGLE),
isGithubEnabled: enabledLoginMethods.includes(LoginMethod.GITHUB),
isGitlabEnabled: enabledLoginMethods.includes(LoginMethod.GITLAB),
isSamlEnabled: enabledLoginMethods.includes(LoginMethod.SAML),
isLdapEnabled: enabledLoginMethods.includes(LoginMethod.LDAP),
isOidcEnabled: enabledLoginMethods.includes(LoginMethod.OIDC)
}
: {
isEmailEnabled: true,
isGoogleEnabled: true,
isGithubEnabled: true,
isGitlabEnabled: true,
isSamlEnabled: true,
isLdapEnabled: true,
isOidcEnabled: true
}
});
const onAuthFormSubmit = async (formData: TAuthForm) => {
try {
const enabledMethods: LoginMethod[] = [];
if (formData.isEmailEnabled) {
enabledMethods.push(LoginMethod.EMAIL);
}
if (formData.isGoogleEnabled) {
enabledMethods.push(LoginMethod.GOOGLE);
}
if (formData.isGithubEnabled) {
enabledMethods.push(LoginMethod.GITHUB);
}
if (formData.isGitlabEnabled) {
enabledMethods.push(LoginMethod.GITLAB);
}
if (formData.isSamlEnabled) {
enabledMethods.push(LoginMethod.SAML);
}
if (formData.isLdapEnabled) {
enabledMethods.push(LoginMethod.LDAP);
}
if (formData.isOidcEnabled) {
enabledMethods.push(LoginMethod.OIDC);
}
if (!enabledMethods.length) {
createNotification({
type: "error",
text: "At least one login method should be enabled."
});
return;
}
await updateServerConfig({
enabledLoginMethods: enabledMethods
});
createNotification({
text: "Login methods have been successfully updated.",
type: "success"
});
} catch (e) {
console.error(e);
createNotification({
type: "error",
text: "Failed to update login methods."
});
}
};
return (
<form
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
onSubmit={handleSubmit(onAuthFormSubmit)}
>
<div className="flex flex-col justify-start">
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Login Methods</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the login methods you wish to allow for all users of this instance.
</div>
<Controller
control={control}
name="isEmailEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="email-enabled"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">Email</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="isGoogleEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="google-enabled"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">Google SSO</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="isGithubEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="enable-github"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">Github SSO</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="isGitlabEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="enable-gitlab"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">Gitlab SSO</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="isSamlEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="enable-saml"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">SAML SSO</p>
</Switch>
</FormControl>
);
}}
/>
<Controller
control={control}
name="isOidcEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="enable-oidc"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">OIDC SSO</p>
</Switch>
</FormControl>
);
}}
/>
</div>
<Controller
control={control}
name="isLdapEnabled"
render={({ field, fieldState: { error } }) => {
return (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Switch
id="enable-ldap"
onCheckedChange={(value) => field.onChange(value)}
isChecked={field.value}
>
<p className="w-24">LDAP</p>
</Switch>
</FormControl>
);
}}
/>
<Button
className="mt-2"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting || !isDirty}
>
Save
</Button>
</form>
);
};
@@ -24,10 +24,12 @@ import {
import { useOrganization, useServerConfig, useUser } from "@app/context";
import { useGetOrganizations, useUpdateServerConfig } from "@app/hooks/api";
import { AuthPanel } from "./AuthPanel";
import { RateLimitPanel } from "./RateLimitPanel";
enum TabSections {
Settings = "settings",
Auth = "auth",
RateLimit = "rate-limit"
}
@@ -120,7 +122,7 @@ export const AdminDashboardPage = () => {
<div className="mx-auto mb-6 w-full max-w-7xl pt-6">
<div className="mb-8 flex flex-col items-start justify-between text-xl">
<h1 className="text-3xl font-semibold">Admin Dashboard</h1>
<p className="text-base text-bunker-300">Manage your Infisical instance.</p>
<p className="text-base text-bunker-300">Manage your instance level configurations.</p>
</div>
</div>
{isUserLoading || isNotAllowed ? (
@@ -131,6 +133,7 @@ export const AdminDashboardPage = () => {
<TabList>
<div className="flex w-full flex-row border-b border-mineshaft-600">
<Tab value={TabSections.Settings}>General</Tab>
<Tab value={TabSections.Auth}>Authentication</Tab>
<Tab value={TabSections.RateLimit}>Rate Limit</Tab>
</div>
</TabList>
@@ -203,7 +206,8 @@ export const AdminDashboardPage = () => {
Default organization
</div>
<div className="mb-4 max-w-sm text-sm text-mineshaft-400">
Select the default organization you want to set for SAML/LDAP based logins. When selected, user logins will be automatically scoped to the selected organization.
Select the default organization you want to set for SAML/LDAP based logins. When
selected, user logins will be automatically scoped to the selected organization.
</div>
<Controller
control={control}
@@ -310,6 +314,9 @@ export const AdminDashboardPage = () => {
</Button>
</form>
</TabPanel>
<TabPanel value={TabSections.Auth}>
<AuthPanel />
</TabPanel>
<TabPanel value={TabSections.RateLimit}>
<RateLimitPanel />
</TabPanel>