mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 09:12:02 +10:00
feat: complete document 2fa (wip)
This commit is contained in:
@ -27,7 +27,7 @@ export type DocumentSigningAuthDialogProps = {
|
||||
actionTarget: FieldType | 'DOCUMENT';
|
||||
open: boolean;
|
||||
onOpenChange: (value: boolean) => void;
|
||||
|
||||
isEnterprise: boolean;
|
||||
/**
|
||||
* The callback to run when the reauth form is filled out.
|
||||
*/
|
||||
@ -41,6 +41,7 @@ export const DocumentSigningAuthDialog = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
onReauthFormSubmit,
|
||||
isEnterprise,
|
||||
}: DocumentSigningAuthDialogProps) => {
|
||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ export interface DocumentSigningAuthProviderProps {
|
||||
recipient: Recipient;
|
||||
user?: SessionUser | null;
|
||||
children: React.ReactNode;
|
||||
isEnterprise: boolean;
|
||||
}
|
||||
|
||||
export const DocumentSigningAuthProvider = ({
|
||||
@ -73,6 +74,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
recipient: initialRecipient,
|
||||
user,
|
||||
children,
|
||||
isEnterprise,
|
||||
}: DocumentSigningAuthProviderProps) => {
|
||||
const [documentAuthOptions, setDocumentAuthOptions] = useState(initialDocumentAuthOptions);
|
||||
const [recipient, setRecipient] = useState(initialRecipient);
|
||||
@ -138,8 +140,13 @@ export const DocumentSigningAuthProvider = ({
|
||||
.exhaustive();
|
||||
|
||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||
// Directly run callback if no auth required.
|
||||
if (!derivedRecipientActionAuth || options.actionTarget !== FieldType.SIGNATURE) {
|
||||
// Determine if authentication is required based on enterprise status and action target.
|
||||
const requiresAuthTrigger = isEnterprise
|
||||
? derivedRecipientActionAuth && options.actionTarget === FieldType.SIGNATURE
|
||||
: derivedRecipientActionAuth && options.actionTarget === 'DOCUMENT';
|
||||
|
||||
// Directly run callback if no auth trigger is needed.
|
||||
if (!requiresAuthTrigger) {
|
||||
await options.onReauthFormSubmit();
|
||||
return;
|
||||
}
|
||||
@ -209,6 +216,7 @@ export const DocumentSigningAuthProvider = ({
|
||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||
actionTarget={documentAuthDialogPayload.actionTarget}
|
||||
documentAuthType={derivedRecipientActionAuth}
|
||||
isEnterprise={isEnterprise}
|
||||
/>
|
||||
)}
|
||||
</DocumentSigningAuthContext.Provider>
|
||||
@ -218,6 +226,8 @@ export const DocumentSigningAuthProvider = ({
|
||||
type ExecuteActionAuthProcedureOptions = Omit<
|
||||
DocumentSigningAuthDialogProps,
|
||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
||||
>;
|
||||
> & {
|
||||
actionTarget: FieldType | 'DOCUMENT';
|
||||
};
|
||||
|
||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
AssistantConfirmationDialog,
|
||||
type NextSigner,
|
||||
} from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
@ -39,6 +40,7 @@ export type DocumentSigningFormProps = {
|
||||
isRecipientsTurn: boolean;
|
||||
allRecipients?: RecipientWithFields[];
|
||||
setSelectedSignerId?: (id: number | null) => void;
|
||||
isEnterprise: boolean;
|
||||
};
|
||||
|
||||
export const DocumentSigningForm = ({
|
||||
@ -49,6 +51,7 @@ export const DocumentSigningForm = ({
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
setSelectedSignerId,
|
||||
isEnterprise,
|
||||
}: DocumentSigningFormProps) => {
|
||||
const { sessionData } = useOptionalSession();
|
||||
const user = sessionData?.user;
|
||||
@ -62,6 +65,7 @@ export const DocumentSigningForm = ({
|
||||
const assistantSignersId = useId();
|
||||
|
||||
const { fullName, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
|
||||
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
|
||||
|
||||
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
|
||||
@ -114,11 +118,17 @@ export const DocumentSigningForm = ({
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
isEnterprise,
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'An error occurred while completing the document. Please try again.',
|
||||
title: _(msg`Error`),
|
||||
description: _(msg`An error occurred while completing the document. Please try again.`),
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
@ -229,7 +239,13 @@ export const DocumentSigningForm = ({
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
isEnterprise,
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
@ -409,7 +425,13 @@ export const DocumentSigningForm = ({
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await executeActionAuthProcedure({
|
||||
actionTarget: 'DOCUMENT',
|
||||
isEnterprise,
|
||||
onReauthFormSubmit: async (authOptions) => {
|
||||
await completeDocument(authOptions, nextSigner);
|
||||
},
|
||||
});
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
|
||||
@ -6,6 +6,7 @@ import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
|
||||
import signingCelebration from '@documenso/assets/images/signing-celebration.png';
|
||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||
@ -60,6 +61,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
throw new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
const isEnterprise = user?.id
|
||||
? await isUserEnterprise({ userId: user.id }).catch(() => false)
|
||||
: false;
|
||||
|
||||
const recipientWithFields = { ...recipient, fields };
|
||||
|
||||
const isRecipientsTurn = await getIsRecipientsTurnToSign({ token });
|
||||
@ -115,6 +120,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
isDocumentAccessValid: false,
|
||||
recipientEmail: recipient.email,
|
||||
recipientHasAccount,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@ -149,6 +155,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
completedFields,
|
||||
recipientSignature,
|
||||
isRecipientsTurn,
|
||||
isEnterprise,
|
||||
} as const);
|
||||
}
|
||||
|
||||
@ -176,6 +183,7 @@ export default function SigningPage() {
|
||||
isRecipientsTurn,
|
||||
allRecipients,
|
||||
recipientWithFields,
|
||||
isEnterprise,
|
||||
} = data;
|
||||
|
||||
if (document.deletedAt || document.status === DocumentStatus.REJECTED) {
|
||||
@ -241,6 +249,7 @@ export default function SigningPage() {
|
||||
documentAuthOptions={document.authOptions}
|
||||
recipient={recipient}
|
||||
user={user}
|
||||
isEnterprise={isEnterprise}
|
||||
>
|
||||
<DocumentSigningPageView
|
||||
recipient={recipientWithFields}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { DocumentVisibility } from '@prisma/client';
|
||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import type { CreateDocumentAuditLogDataResponse } from '@documenso/lib/utils/document-audit-logs';
|
||||
@ -135,18 +133,18 @@ export const updateDocument = async ({
|
||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (newGlobalActionAuth) {
|
||||
const isDocumentEnterprise = await isUserEnterprise({
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
// if (newGlobalActionAuth) {
|
||||
// const isDocumentEnterprise = await isUserEnterprise({
|
||||
// userId,
|
||||
// teamId,
|
||||
// });
|
||||
|
||||
if (!isDocumentEnterprise) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
}
|
||||
// if (!isDocumentEnterprise) {
|
||||
// throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
// message: 'You do not have permission to set the action auth',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
const isTitleSame = data.title === undefined || data.title === document.title;
|
||||
const isExternalIdSame = data.externalId === undefined || data.externalId === document.externalId;
|
||||
|
||||
@ -2,6 +2,7 @@ import { DocumentStatus, FieldType, RecipientRole, SigningStatus } from '@prisma
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
|
||||
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
|
||||
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
|
||||
@ -13,7 +14,7 @@ import { prisma } from '@documenso/prisma';
|
||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '../../constants/time-zones';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||
import type { TRecipientActionAuth, TRecipientActionAuthTypes } from '../../types/document-auth';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
@ -23,6 +24,7 @@ import {
|
||||
} from '../../types/field-meta';
|
||||
import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||
import { validateFieldAuth } from '../document/validate-field-auth';
|
||||
|
||||
export type SignFieldWithTokenOptions = {
|
||||
@ -169,13 +171,24 @@ export const signFieldWithToken = async ({
|
||||
}
|
||||
}
|
||||
|
||||
const derivedRecipientActionAuth = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
authOptions,
|
||||
});
|
||||
const isEnterprise = userId ? await isUserEnterprise({ userId }) : false;
|
||||
let requiredAuthType: TRecipientActionAuthTypes | null = null;
|
||||
|
||||
if (isEnterprise) {
|
||||
requiredAuthType = await validateFieldAuth({
|
||||
documentAuthOptions: document.authOptions,
|
||||
recipient,
|
||||
field,
|
||||
userId,
|
||||
authOptions,
|
||||
});
|
||||
} else {
|
||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
recipientAuth: recipient.authOptions,
|
||||
});
|
||||
requiredAuthType = derivedRecipientActionAuth;
|
||||
}
|
||||
|
||||
const documentMeta = await prisma.documentMeta.findFirst({
|
||||
where: {
|
||||
@ -286,9 +299,9 @@ export const signFieldWithToken = async ({
|
||||
}),
|
||||
)
|
||||
.exhaustive(),
|
||||
fieldSecurity: derivedRecipientActionAuth
|
||||
fieldSecurity: requiredAuthType
|
||||
? {
|
||||
type: derivedRecipientActionAuth,
|
||||
type: requiredAuthType,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
|
||||
@ -68,6 +68,16 @@ export const ZDocumentActionAuthTypesSchema = z
|
||||
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
||||
);
|
||||
|
||||
/**
|
||||
* The non-enterprise document action auth methods.
|
||||
*
|
||||
* Only includes options available to non-enterprise users.
|
||||
*/
|
||||
export const ZNonEnterpriseDocumentActionAuthTypesSchema = z.enum([
|
||||
DocumentAuth.TWO_FACTOR_AUTH,
|
||||
DocumentAuth.EXPLICIT_NONE,
|
||||
]);
|
||||
|
||||
/**
|
||||
* The recipient access auth methods.
|
||||
*
|
||||
@ -102,6 +112,7 @@ export const ZRecipientActionAuthTypesSchema = z
|
||||
|
||||
export const DocumentAccessAuth = ZDocumentAccessAuthTypesSchema.Enum;
|
||||
export const DocumentActionAuth = ZDocumentActionAuthTypesSchema.Enum;
|
||||
export const NonEnterpriseDocumentActionAuth = ZNonEnterpriseDocumentActionAuthTypesSchema.Enum;
|
||||
export const RecipientAccessAuth = ZRecipientAccessAuthTypesSchema.Enum;
|
||||
export const RecipientActionAuth = ZRecipientActionAuthTypesSchema.Enum;
|
||||
|
||||
@ -152,6 +163,9 @@ export type TDocumentAccessAuth = z.infer<typeof ZDocumentAccessAuthSchema>;
|
||||
export type TDocumentAccessAuthTypes = z.infer<typeof ZDocumentAccessAuthTypesSchema>;
|
||||
export type TDocumentActionAuth = z.infer<typeof ZDocumentActionAuthSchema>;
|
||||
export type TDocumentActionAuthTypes = z.infer<typeof ZDocumentActionAuthTypesSchema>;
|
||||
export type TNonEnterpriseDocumentActionAuthTypes = z.infer<
|
||||
typeof ZNonEnterpriseDocumentActionAuthTypesSchema
|
||||
>;
|
||||
export type TRecipientAccessAuth = z.infer<typeof ZRecipientAccessAuthSchema>;
|
||||
export type TRecipientAccessAuthTypes = z.infer<typeof ZRecipientAccessAuthTypesSchema>;
|
||||
export type TRecipientActionAuth = z.infer<typeof ZRecipientActionAuthSchema>;
|
||||
|
||||
@ -7,7 +7,11 @@ import type { SelectProps } from '@radix-ui/react-select';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||
import { DocumentActionAuth, DocumentAuth } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
DocumentActionAuth,
|
||||
DocumentAuth,
|
||||
NonEnterpriseDocumentActionAuth,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -17,38 +21,51 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
||||
|
||||
export const DocumentGlobalAuthActionSelect = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
(props, ref) => {
|
||||
const { _ } = useLingui();
|
||||
interface DocumentGlobalAuthActionSelectProps extends SelectProps {
|
||||
isDocumentEnterprise?: boolean;
|
||||
}
|
||||
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
ref={ref}
|
||||
data-testid="documentActionSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
export const DocumentGlobalAuthActionSelect = forwardRef<
|
||||
HTMLButtonElement,
|
||||
DocumentGlobalAuthActionSelectProps
|
||||
>(({ isDocumentEnterprise, ...props }, ref) => {
|
||||
const { _ } = useLingui();
|
||||
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
return (
|
||||
<Select {...props}>
|
||||
<SelectTrigger className="bg-background text-muted-foreground">
|
||||
<SelectValue
|
||||
ref={ref}
|
||||
data-testid="documentActionSelectValue"
|
||||
placeholder={_(msg`No restrictions`)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
{Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
},
|
||||
);
|
||||
<SelectContent position="popper">
|
||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||
<SelectItem value={'-1'}>
|
||||
<Trans>No restrictions</Trans>
|
||||
</SelectItem>
|
||||
|
||||
{isDocumentEnterprise
|
||||
? Object.values(DocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))
|
||||
: Object.values(NonEnterpriseDocumentActionAuth)
|
||||
.filter((auth) => auth !== DocumentAuth.EXPLICIT_NONE)
|
||||
.map((authType) => (
|
||||
<SelectItem key={authType} value={authType}>
|
||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
|
||||
DocumentGlobalAuthActionSelect.displayName = 'DocumentGlobalAuthActionSelect';
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@prisma/client';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
DocumentStatus,
|
||||
DocumentVisibility,
|
||||
type Field,
|
||||
type Recipient,
|
||||
SendStatus,
|
||||
TeamMemberRole,
|
||||
} from '@prisma/client';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
@ -268,24 +273,22 @@ export const AddSettingsFormPartial = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDocumentEnterprise && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="globalActionAuth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
<DocumentGlobalAuthActionTooltip />
|
||||
</FormLabel>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="globalActionAuth"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="flex flex-row items-center">
|
||||
<Trans>Recipient action authentication</Trans>
|
||||
<DocumentGlobalAuthActionTooltip />
|
||||
</FormLabel>
|
||||
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormControl>
|
||||
<DocumentGlobalAuthActionSelect {...field} onValueChange={field.onChange} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Accordion type="multiple" className="mt-6">
|
||||
<AccordionItem value="advanced-options" className="border-none">
|
||||
|
||||
Reference in New Issue
Block a user