mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 11:38:55 +00:00
Merge remote-tracking branch 'origin' into token-auth
This commit is contained in:
Generated
+2
-3
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
+15
-6
@@ -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"
|
||||
|
||||
+9
-7
@@ -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} />}
|
||||
>
|
||||
|
||||
+18
-20
@@ -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>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
+14
-24
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ type Props = {
|
||||
status: "close" | "open";
|
||||
approvals: number;
|
||||
canApprove?: boolean;
|
||||
statusChangeByEmail: string;
|
||||
statusChangeByEmail?: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
|
||||
+30
-33
@@ -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>
|
||||
|
||||
+32
-5
@@ -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>
|
||||
)}
|
||||
|
||||
+1
@@ -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>
|
||||
);
|
||||
|
||||
+1
-1
@@ -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)}
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+16
-7
@@ -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">
|
||||
|
||||
+104
-43
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user