mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 21:29:20 +00:00
Merge remote-tracking branch 'origin' into email-case-sensitive
This commit is contained in:
@@ -29,7 +29,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
"fixed top-1/2 left-1/2 z-[90] max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
"fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,4 +4,5 @@ export type ServerStatus = {
|
||||
emailConfigured: boolean;
|
||||
inviteOnlySignup: boolean;
|
||||
secretScanningConfigured: boolean
|
||||
redisConfigured: boolean
|
||||
};
|
||||
@@ -16,6 +16,7 @@ export {
|
||||
useGetWorkspaceUsers,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
useReorderWsEnvironment,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment} from "./queries";
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
GetWsEnvironmentDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
RenameWorkspaceDTO,
|
||||
ReorderEnvironmentsDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
Workspace,
|
||||
@@ -244,6 +245,21 @@ export const useCreateWsEnvironment = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useReorderWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, ReorderEnvironmentsDTO>({
|
||||
mutationFn: ({ workspaceID, environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName}) => {
|
||||
return apiRequest.patch(`/api/v2/workspace/${workspaceID}/environments`, {
|
||||
environmentSlug, environmentName, otherEnvironmentSlug, otherEnvironmentName
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWsEnvironment = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -46,6 +46,15 @@ export type CreateEnvironmentDTO = {
|
||||
environmentName: string;
|
||||
};
|
||||
|
||||
export type ReorderEnvironmentsDTO = {
|
||||
workspaceID: string;
|
||||
environmentSlug: string;
|
||||
environmentName: string;
|
||||
otherEnvironmentSlug: string;
|
||||
otherEnvironmentName: string;
|
||||
|
||||
};
|
||||
|
||||
export type UpdateEnvironmentDTO = {
|
||||
workspaceID: string;
|
||||
oldEnvironmentSlug: string;
|
||||
|
||||
@@ -483,7 +483,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
@@ -491,11 +491,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs V2
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link> */}
|
||||
<Link href={`/project/${currentWorkspace?._id}/logs`} passHref>
|
||||
</Link>
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
@@ -506,7 +506,7 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
</Link> */}
|
||||
{/* <Link href={`/project/${currentWorkspace?._id}/secret-scanning`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { IconProp } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faSlack } from "@fortawesome/free-brands-svg-icons";
|
||||
@@ -11,6 +12,7 @@ import { faFolderOpen } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCheckCircle,
|
||||
faExclamationCircle,
|
||||
faHandPeace,
|
||||
faMagnifyingGlass,
|
||||
faNetworkWired,
|
||||
@@ -37,6 +39,7 @@ import {
|
||||
import { TabsObject } from "@app/components/v2/Tabs";
|
||||
import { useSubscription, useUser, useWorkspace } from "@app/context";
|
||||
import { fetchOrgUsers, useAddUserToWs, useCreateWorkspace, useRegisterUserAction,useUploadWsKey } from "@app/hooks/api";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { encryptAssymmetric } from "../../../../components/utilities/cryptography/crypto";
|
||||
@@ -269,6 +272,8 @@ export default function Organization() {
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const uploadWsKey = useUploadWsKey();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
|
||||
|
||||
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {
|
||||
// type check
|
||||
@@ -340,6 +345,18 @@ export default function Organization() {
|
||||
<title>{t("common.head-title", { title: t("settings.members.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
{!serverDetails?.redisConfigured && <div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
<p className="mr-4 mb-4 font-semibold text-white">Announcements</p>
|
||||
<div className="w-full border border-blue-400/70 rounded-md bg-blue-900/70 p-2 text-base text-mineshaft-100 flex items-center">
|
||||
<FontAwesomeIcon icon={faExclamationCircle} className="text-2xl mr-4 p-4 text-mineshaft-50"/>
|
||||
Attention: Updated versions of Infisical now require Redis for full functionality. Learn how to configure it
|
||||
<Link href="https://infisical.com/docs/self-hosting/configuration/redis" target="_blank">
|
||||
<span className="pl-1 text-white underline underline-offset-2 hover:decoration-blue-400 hover:text-blue-200 duration-100 cursor-pointer">
|
||||
here
|
||||
</span>
|
||||
</Link>.
|
||||
</div>
|
||||
</div>}
|
||||
<div className="mb-4 flex flex-col items-start justify-start px-6 py-6 pb-0 text-3xl">
|
||||
<p className="mr-4 font-semibold text-white">Projects</p>
|
||||
<div className="mt-6 flex w-full flex-row">
|
||||
|
||||
@@ -9,7 +9,7 @@ const Logs = () => {
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
|
||||
@@ -110,6 +110,11 @@ export const SecretInputRow = memo(
|
||||
append
|
||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||
|
||||
const tagColorByTagId = new Map((wsTags || []).map((wsTag, i) => [wsTag._id, tagColors[i % tagColors.length]]))
|
||||
|
||||
// display the tags in alphabetical order
|
||||
secretTags.sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
// to get details on a secret
|
||||
const overrideAction = useWatch({
|
||||
control,
|
||||
@@ -321,19 +326,22 @@ export const SecretInputRow = memo(
|
||||
</td>
|
||||
<td className="min-w-sm flex">
|
||||
<div className="flex h-8 items-center pl-2">
|
||||
{secretTags.map(({ id, slug }, i) => (
|
||||
<Tag
|
||||
className={cx(
|
||||
tagColors[i % tagColors.length].bg,
|
||||
tagColors[i % tagColors.length].text
|
||||
)}
|
||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
onClose={() => remove(i)}
|
||||
key={id}
|
||||
>
|
||||
{slug}
|
||||
</Tag>
|
||||
))}
|
||||
{secretTags.map(({ id, _id, slug }, i) => {
|
||||
// This map lookup shouldn't ever fail, but if it does we default to the first color
|
||||
const tagColor = tagColorByTagId.get(_id) || tagColors[0]
|
||||
return (
|
||||
<Tag
|
||||
className={cx(
|
||||
tagColor.bg,
|
||||
tagColor.text
|
||||
)}
|
||||
isDisabled={isReadOnly || isAddOnly || isRollbackMode}
|
||||
onClose={() => remove(i)}
|
||||
key={id}
|
||||
>
|
||||
{slug}
|
||||
</Tag>)
|
||||
})}
|
||||
<div className="w-0 overflow-hidden group-hover:w-6">
|
||||
<Tooltip content="Copy value">
|
||||
<IconButton
|
||||
|
||||
@@ -40,7 +40,8 @@ export const PasswordStep = ({
|
||||
authMethod
|
||||
} = jwt_decode(providerAuthToken) as any;
|
||||
|
||||
const handleLogin = async () => {
|
||||
const handleLogin = async (e:React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -119,10 +120,12 @@ export const PasswordStep = ({
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
onSubmit={handleLogin}
|
||||
className="h-full mx-auto w-full max-w-md px-6 pt-8"
|
||||
>
|
||||
<div className="mb-8">
|
||||
@@ -153,9 +156,9 @@ export const PasswordStep = ({
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[22rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={async () => handleLogin()}
|
||||
isFullWidth
|
||||
isLoading={isLoading}
|
||||
className="h-14"
|
||||
|
||||
@@ -15,11 +15,19 @@ export const SAMLSSOStep = ({
|
||||
|
||||
const queryParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const handleSubmission = (e:React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
window.close();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-md md:px-6">
|
||||
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||
What's your SSO Identifier?
|
||||
</p>
|
||||
<form onSubmit={handleSubmission}>
|
||||
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
||||
<Input
|
||||
@@ -36,19 +44,16 @@ export const SAMLSSOStep = ({
|
||||
</div>
|
||||
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
|
||||
<Button
|
||||
type="submit"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
const callbackPort = queryParams.get("callback_port");
|
||||
window.open(`/api/v1/sso/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||
window.close();
|
||||
}}
|
||||
isFullWidth
|
||||
className="h-14"
|
||||
>
|
||||
{t("login.login")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-row items-center justify-center mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -74,8 +74,7 @@ export const LogsFilter = ({
|
||||
className="w-40 mr-4"
|
||||
>
|
||||
<Select
|
||||
placeholder="Select"
|
||||
// {...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full bg-mineshaft-700 border border-mineshaft-500 text-mineshaft-100"
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
export const LogsSection = () => {
|
||||
const { subscription } = useSubscription();
|
||||
const router = useRouter();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@@ -20,6 +32,12 @@ export const LogsSection = () => {
|
||||
perPage: 10
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (subscription && !subscription.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}
|
||||
}, [subscription]);
|
||||
|
||||
const eventType = watch("eventType") as EventType | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
@@ -54,6 +72,19 @@ export const LogsSection = () => {
|
||||
perPage={perPage}
|
||||
setValue={setValue}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
|
||||
if (!isOpen) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpToggle("upgradePlan", isOpen)
|
||||
}}
|
||||
text="You can use audit logs if you switch to a paid Infisical plan."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+4
-2
@@ -52,7 +52,7 @@ export const SecretOverviewTableRow = ({
|
||||
<div className="text-blue-300/70">
|
||||
<FontAwesomeIcon icon={isFormExpanded ? faAngleDown : faKey} />
|
||||
</div>
|
||||
<div>{secretKey}</div>
|
||||
<div title={secretKey}>{secretKey}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
@@ -73,7 +73,9 @@ export const SecretOverviewTableRow = ({
|
||||
>
|
||||
<div className="h-full w-full border-r border-mineshaft-600 py-[0.85rem] px-5">
|
||||
<div className="flex justify-center">
|
||||
{!isSecretEmpty && <FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />}
|
||||
{!isSecretEmpty && <Tooltip content={isSecretPresent ? "Present secret" : "Missing secret"}>
|
||||
<FontAwesomeIcon icon={isSecretPresent ? faCheck : faXmark} />
|
||||
</Tooltip>}
|
||||
{isSecretEmpty && (
|
||||
<Tooltip content="Empty value">
|
||||
<FontAwesomeIcon icon={faCircle} />
|
||||
|
||||
+67
-2
@@ -1,6 +1,7 @@
|
||||
import { faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faArrowDown,faArrowUp, faPencil, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
EmptyState,
|
||||
IconButton,
|
||||
@@ -14,6 +15,9 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useReorderWsEnvironment
|
||||
} from "@app/hooks/api";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -31,6 +35,43 @@ type Props = {
|
||||
|
||||
export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
const { currentWorkspace, isLoading } = useWorkspace();
|
||||
const { createNotification } = useNotificationContext();
|
||||
const reorderWsEnvironment = useReorderWsEnvironment();
|
||||
|
||||
const handleReorderEnv= async (shouldMoveUp: boolean, name: string, slug: string) => {
|
||||
try {
|
||||
if (!currentWorkspace?._id) return;
|
||||
|
||||
const indexOfEnv = currentWorkspace.environments.findIndex((env) => env.name === name && env.slug === slug);
|
||||
|
||||
// check that this reordering is possible
|
||||
if (indexOfEnv === 0 && shouldMoveUp || indexOfEnv === currentWorkspace.environments.length - 1 && !shouldMoveUp) {
|
||||
return
|
||||
}
|
||||
|
||||
const indexToSwap = shouldMoveUp ? indexOfEnv - 1 : indexOfEnv + 1
|
||||
|
||||
await reorderWsEnvironment.mutateAsync({
|
||||
workspaceID: currentWorkspace._id,
|
||||
environmentSlug: slug,
|
||||
environmentName: name,
|
||||
otherEnvironmentSlug: currentWorkspace.environments[indexToSwap].slug,
|
||||
otherEnvironmentName: currentWorkspace.environments[indexToSwap].name
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully re-ordered environments",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to re-order environments",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
@@ -45,11 +86,35 @@ export const EnvironmentTable = ({ handlePopUpOpen }: Props) => {
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="project-envs" />}
|
||||
{!isLoading &&
|
||||
currentWorkspace &&
|
||||
currentWorkspace.environments.map(({ name, slug }) => (
|
||||
currentWorkspace.environments.map(({ name, slug }, pos) => (
|
||||
<Tr key={name}>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td className="flex items-center justify-end">
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(false, name, slug)
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === currentWorkspace.environments.length - 1}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
handleReorderEnv(true, name, slug)
|
||||
}}
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
isDisabled={pos === 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
className="mr-3 py-2"
|
||||
onClick={() => {
|
||||
|
||||
+2
-5
@@ -11,16 +11,13 @@ import {
|
||||
useNameWorkspaceSecrets
|
||||
} from "@app/hooks/api";
|
||||
|
||||
// TODO: add check so that this only shows up if user is
|
||||
// an admin in the workspace
|
||||
|
||||
export const ProjectIndexSecretsSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: isBlindIndexed, isLoading: isBlindIndexedLoading } = useGetWorkspaceIndexStatus(currentWorkspace?._id ?? "");
|
||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?._id ?? "");
|
||||
const { data: encryptedSecrets } = useGetWorkspaceSecrets(currentWorkspace?._id ?? "");
|
||||
const nameWorkspaceSecrets = useNameWorkspaceSecrets();
|
||||
|
||||
|
||||
const onEnableBlindIndices = async () => {
|
||||
if (!currentWorkspace?._id) return;
|
||||
if (!encryptedSecrets) return;
|
||||
@@ -53,7 +50,7 @@ export const ProjectIndexSecretsSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
return (!isBlindIndexedLoading && !isBlindIndexed) ? (
|
||||
return (!isBlindIndexedLoading && (isBlindIndexed === false)) ? (
|
||||
<div className="mb-6 p-4 bg-mineshaft-900 rounded-lg border border-mineshaft-600">
|
||||
<p className="mb-3 text-xl font-semibold">Blind Indices</p>
|
||||
<p className="text-gray-400 mb-8">
|
||||
|
||||
Reference in New Issue
Block a user