mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
@@ -1,4 +1,4 @@
|
|||||||
iame: captcha_remediation
|
name: captcha_remediation
|
||||||
filters:
|
filters:
|
||||||
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
|
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
|
||||||
decisions:
|
decisions:
|
||||||
@@ -22,4 +22,4 @@ filters:
|
|||||||
decisions:
|
decisions:
|
||||||
- type: ban
|
- type: ban
|
||||||
duration: 4h
|
duration: 4h
|
||||||
on_success: break
|
on_success: break
|
||||||
|
|||||||
@@ -1162,7 +1162,7 @@
|
|||||||
"selectDomainTypeCnameName": "Single Domain (CNAME)",
|
"selectDomainTypeCnameName": "Single Domain (CNAME)",
|
||||||
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
|
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
|
||||||
"selectDomainTypeWildcardName": "Wildcard Domain",
|
"selectDomainTypeWildcardName": "Wildcard Domain",
|
||||||
"selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.",
|
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
|
||||||
"domainDelegation": "Single Domain",
|
"domainDelegation": "Single Domain",
|
||||||
"selectType": "Select a type",
|
"selectType": "Select a type",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ function createEmailClient() {
|
|||||||
host: emailConfig.smtp_host,
|
host: emailConfig.smtp_host,
|
||||||
port: emailConfig.smtp_port,
|
port: emailConfig.smtp_port,
|
||||||
secure: emailConfig.smtp_secure || false,
|
secure: emailConfig.smtp_secure || false,
|
||||||
auth: {
|
auth: (emailConfig.smtp_user && emailConfig.smtp_pass) ? {
|
||||||
user: emailConfig.smtp_user,
|
user: emailConfig.smtp_user,
|
||||||
pass: emailConfig.smtp_pass
|
pass: emailConfig.smtp_pass
|
||||||
}
|
} : null
|
||||||
} as SMTPTransport.Options;
|
} as SMTPTransport.Options;
|
||||||
|
|
||||||
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {
|
if (emailConfig.smtp_tls_reject_unauthorized !== undefined) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.7.0";
|
export const APP_VERSION = "1.7.3";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|||||||
@@ -620,8 +620,6 @@ authenticated.post(
|
|||||||
|
|
||||||
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
|
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
|
||||||
|
|
||||||
authenticated.get("/idp", verifyUserIsServerAdmin, idp.listIdps);
|
|
||||||
|
|
||||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||||
|
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ export async function validateOidcCallback(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug("State verified", {
|
||||||
|
urL: ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
|
||||||
|
expectedState,
|
||||||
|
state
|
||||||
|
});
|
||||||
|
|
||||||
const tokens = await client.validateAuthorizationCode(
|
const tokens = await client.validateAuthorizationCode(
|
||||||
ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
|
ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl),
|
||||||
code,
|
code,
|
||||||
|
|||||||
@@ -261,14 +261,6 @@ async function createHttpResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (parsedSubdomain.data.includes(".")) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Subdomain cannot contain a dot when using wildcard domains"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
} else {
|
} else {
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
|||||||
@@ -297,14 +297,6 @@ async function updateHttpResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (parsedSubdomain.data.includes(".")) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Subdomain cannot contain a dot when using wildcard domains"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
fullDomain = `${updateData.subdomain}.${domainRes.domains.baseDomain}`;
|
||||||
} else {
|
} else {
|
||||||
fullDomain = domainRes.domains.baseDomain;
|
fullDomain = domainRes.domains.baseDomain;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { StrategySelect } from "@app/components/StrategySelect";
|
import { StrategyOption, StrategySelect } from "@app/components/StrategySelect";
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
@@ -45,15 +45,10 @@ import { createApiClient } from "@app/lib/api";
|
|||||||
import { Checkbox } from "@app/components/ui/checkbox";
|
import { Checkbox } from "@app/components/ui/checkbox";
|
||||||
import { ListIdpsResponse } from "@server/routers/idp";
|
import { ListIdpsResponse } from "@server/routers/idp";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
type UserType = "internal" | "oidc";
|
type UserType = "internal" | "oidc";
|
||||||
|
|
||||||
interface UserTypeOption {
|
|
||||||
id: UserType;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdpOption {
|
interface IdpOption {
|
||||||
idpId: number;
|
idpId: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -147,13 +142,20 @@ export default function Page() {
|
|||||||
}
|
}
|
||||||
}, [userType, env.email.emailEnabled, internalForm, externalForm]);
|
}, [userType, env.email.emailEnabled, internalForm, externalForm]);
|
||||||
|
|
||||||
const userTypes: UserTypeOption[] = [
|
const [userTypes, setUserTypes] = useState<StrategyOption<string>[]>([
|
||||||
{
|
{
|
||||||
id: "internal",
|
id: "internal",
|
||||||
title: t("userTypeInternal"),
|
title: t("userTypeInternal"),
|
||||||
description: t("userTypeInternalDescription")
|
description: t("userTypeInternalDescription"),
|
||||||
|
disabled: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "oidc",
|
||||||
|
title: t("userTypeExternal"),
|
||||||
|
description: t("userTypeExternalDescription"),
|
||||||
|
disabled: true
|
||||||
}
|
}
|
||||||
];
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!userType) {
|
if (!userType) {
|
||||||
@@ -177,9 +179,6 @@ export default function Page() {
|
|||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
setRoles(res.data.data.roles);
|
setRoles(res.data.data.roles);
|
||||||
if (userType === "internal") {
|
|
||||||
setDataLoaded(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,24 +199,32 @@ export default function Page() {
|
|||||||
|
|
||||||
if (res?.status === 200) {
|
if (res?.status === 200) {
|
||||||
setIdps(res.data.data.idps);
|
setIdps(res.data.data.idps);
|
||||||
setDataLoaded(true);
|
|
||||||
|
|
||||||
if (res.data.data.idps.length) {
|
if (res.data.data.idps.length) {
|
||||||
userTypes.push({
|
setUserTypes((prev) =>
|
||||||
id: "oidc",
|
prev.map((type) => {
|
||||||
title: t("userTypeExternal"),
|
if (type.id === "oidc") {
|
||||||
description: t("userTypeExternalDescription")
|
return {
|
||||||
});
|
...type,
|
||||||
|
disabled: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDataLoaded(false);
|
async function fetchInitialData() {
|
||||||
fetchRoles();
|
setDataLoaded(false);
|
||||||
if (userType !== "internal") {
|
await fetchRoles();
|
||||||
fetchIdps();
|
await fetchIdps();
|
||||||
|
setDataLoaded(true);
|
||||||
}
|
}
|
||||||
}, [userType]);
|
|
||||||
|
fetchInitialData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
async function onSubmitInternal(
|
async function onSubmitInternal(
|
||||||
values: z.infer<typeof internalFormSchema>
|
values: z.infer<typeof internalFormSchema>
|
||||||
@@ -323,7 +330,7 @@ export default function Page() {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
{!inviteLink && userTypes.length > 1 ? (
|
{!inviteLink && build !== "saas" ? (
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionTitle>
|
||||||
@@ -610,7 +617,7 @@ export default function Page() {
|
|||||||
idp || null
|
idp || null
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
cols={3}
|
cols={2}
|
||||||
/>
|
/>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect";
|
|||||||
import { Layout } from "@app/components/Layout";
|
import { Layout } from "@app/components/Layout";
|
||||||
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
import { InitialSetupCompleteResponse } from "@server/routers/auth";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -83,25 +84,27 @@ export default async function Page(props: {
|
|||||||
if (ownedOrg) {
|
if (ownedOrg) {
|
||||||
redirect(`/${ownedOrg.orgId}`);
|
redirect(`/${ownedOrg.orgId}`);
|
||||||
} else {
|
} else {
|
||||||
redirect("/setup");
|
if (!env.flags.disableUserCreateOrg || user.serverAdmin) {
|
||||||
|
redirect("/setup");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// return (
|
return (
|
||||||
// <UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
// <Layout orgs={orgs} navItems={[]}>
|
<Layout orgs={orgs} navItems={[]}>
|
||||||
// <div className="w-full max-w-md mx-auto md:mt-32 mt-4">
|
<div className="w-full max-w-md mx-auto md:mt-32 mt-4">
|
||||||
// <OrganizationLanding
|
<OrganizationLanding
|
||||||
// disableCreateOrg={
|
disableCreateOrg={
|
||||||
// env.flags.disableUserCreateOrg && !user.serverAdmin
|
env.flags.disableUserCreateOrg && !user.serverAdmin
|
||||||
// }
|
}
|
||||||
// organizations={orgs.map((org) => ({
|
organizations={orgs.map((org) => ({
|
||||||
// name: org.name,
|
name: org.name,
|
||||||
// id: org.orgId
|
id: org.orgId
|
||||||
// }))}
|
}))}
|
||||||
// />
|
/>
|
||||||
// </div>
|
</div>
|
||||||
// </Layout>
|
</Layout>
|
||||||
// </UserProvider>
|
</UserProvider>
|
||||||
// );
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export default function DomainPicker({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (orgDomain.type === "wildcard") {
|
} else if (orgDomain.type === "wildcard") {
|
||||||
// For wildcard domains, allow the base domain or one level up
|
// For wildcard domains, allow the base domain or multiple levels up
|
||||||
const userInputLower = userInput.toLowerCase();
|
const userInputLower = userInput.toLowerCase();
|
||||||
const baseDomainLower = orgDomain.baseDomain.toLowerCase();
|
const baseDomainLower = orgDomain.baseDomain.toLowerCase();
|
||||||
|
|
||||||
@@ -194,24 +194,22 @@ export default function DomainPicker({
|
|||||||
domainId: orgDomain.domainId
|
domainId: orgDomain.domainId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Check if user input is one level up (subdomain.baseDomain)
|
// Check if user input ends with the base domain (allows multiple level subdomains)
|
||||||
else if (userInputLower.endsWith(`.${baseDomainLower}`)) {
|
else if (userInputLower.endsWith(`.${baseDomainLower}`)) {
|
||||||
const subdomain = userInputLower.slice(
|
const subdomain = userInputLower.slice(
|
||||||
0,
|
0,
|
||||||
-(baseDomainLower.length + 1)
|
-(baseDomainLower.length + 1)
|
||||||
);
|
);
|
||||||
// Only allow one level up (no dots in subdomain)
|
// Allow multiple levels (subdomain can contain dots)
|
||||||
if (!subdomain.includes(".")) {
|
options.push({
|
||||||
options.push({
|
id: `org-${orgDomain.domainId}`,
|
||||||
id: `org-${orgDomain.domainId}`,
|
domain: userInput,
|
||||||
domain: userInput,
|
type: "organization",
|
||||||
type: "organization",
|
verified: orgDomain.verified,
|
||||||
verified: orgDomain.verified,
|
domainType: "wildcard",
|
||||||
domainType: "wildcard",
|
domainId: orgDomain.domainId,
|
||||||
domainId: orgDomain.domainId,
|
subdomain: subdomain
|
||||||
subdomain: subdomain
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -320,7 +318,7 @@ export default function DomainPicker({
|
|||||||
setUserInput(validInput);
|
setUserInput(validInput);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{build === "saas"
|
{build === "saas"
|
||||||
? t("domainPickerDescriptionSaas")
|
? t("domainPickerDescriptionSaas")
|
||||||
: t("domainPickerDescription")}
|
: t("domainPickerDescription")}
|
||||||
@@ -328,42 +326,44 @@ export default function DomainPicker({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs and Sort Toggle */}
|
{/* Tabs and Sort Toggle */}
|
||||||
<div className="flex justify-between items-center">
|
{build === "saas" && (
|
||||||
<Tabs
|
<div className="flex justify-between items-center">
|
||||||
value={activeTab}
|
<Tabs
|
||||||
onValueChange={(value) =>
|
value={activeTab}
|
||||||
setActiveTab(
|
onValueChange={(value) =>
|
||||||
value as "all" | "organization" | "provided"
|
setActiveTab(
|
||||||
)
|
value as "all" | "organization" | "provided"
|
||||||
}
|
)
|
||||||
>
|
}
|
||||||
<TabsList>
|
>
|
||||||
<TabsTrigger value="all">
|
<TabsList>
|
||||||
{t("domainPickerTabAll")}
|
<TabsTrigger value="all">
|
||||||
</TabsTrigger>
|
{t("domainPickerTabAll")}
|
||||||
<TabsTrigger value="organization">
|
|
||||||
{t("domainPickerTabOrganization")}
|
|
||||||
</TabsTrigger>
|
|
||||||
{build == "saas" && (
|
|
||||||
<TabsTrigger value="provided">
|
|
||||||
{t("domainPickerTabProvided")}
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
<TabsTrigger value="organization">
|
||||||
</TabsList>
|
{t("domainPickerTabOrganization")}
|
||||||
</Tabs>
|
</TabsTrigger>
|
||||||
<Button
|
{build == "saas" && (
|
||||||
variant="outline"
|
<TabsTrigger value="provided">
|
||||||
size="sm"
|
{t("domainPickerTabProvided")}
|
||||||
onClick={() =>
|
</TabsTrigger>
|
||||||
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
|
)}
|
||||||
}
|
</TabsList>
|
||||||
>
|
</Tabs>
|
||||||
<ArrowUpDown className="h-4 w-4 mr-2" />
|
<Button
|
||||||
{sortOrder === "asc"
|
variant="outline"
|
||||||
? t("domainPickerSortAsc")
|
size="sm"
|
||||||
: t("domainPickerSortDesc")}
|
onClick={() =>
|
||||||
</Button>
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc")
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="h-4 w-4 mr-2" />
|
||||||
|
{sortOrder === "asc"
|
||||||
|
? t("domainPickerSortAsc")
|
||||||
|
: t("domainPickerSortDesc")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading State */}
|
||||||
{isChecking && (
|
{isChecking && (
|
||||||
|
|||||||
@@ -41,35 +41,31 @@ export default function OrganizationLanding({
|
|||||||
function getDescriptionText() {
|
function getDescriptionText() {
|
||||||
if (organizations.length === 0) {
|
if (organizations.length === 0) {
|
||||||
if (!disableCreateOrg) {
|
if (!disableCreateOrg) {
|
||||||
return t('componentsErrorNoMemberCreate');
|
return t("componentsErrorNoMemberCreate");
|
||||||
} else {
|
} else {
|
||||||
return t('componentsErrorNoMember');
|
return t("componentsErrorNoMember");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('componentsMember', {count: organizations.length});
|
return t("componentsMember", { count: organizations.length });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{t('welcome')}</CardTitle>
|
<CardTitle>{t("welcome")}</CardTitle>
|
||||||
<CardDescription>{getDescriptionText()}</CardDescription>
|
<CardDescription>{getDescriptionText()}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{organizations.length === 0 ? (
|
{organizations.length === 0 ? (
|
||||||
disableCreateOrg ? (
|
!disableCreateOrg && (
|
||||||
<p className="text-center text-muted-foreground">
|
|
||||||
t('componentsErrorNoMember')
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<Link href="/setup">
|
<Link href="/setup">
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-auto py-3 text-lg"
|
className="w-full h-auto py-3 text-lg"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-5 w-5" />
|
<Plus className="mr-2 h-5 w-5" />
|
||||||
{t('componentsCreateOrg')}
|
{t("componentsCreateOrg")}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,8 +103,10 @@ export default function SecurityKeyForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSecurityKeys();
|
if (open) {
|
||||||
}, []);
|
loadSecurityKeys();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
name: z.string().min(1, { message: t("securityKeyNameRequired") }),
|
name: z.string().min(1, { message: t("securityKeyNameRequired") }),
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { cn } from "@app/lib/cn";
|
|||||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface StrategyOption<TValue extends string> {
|
export interface StrategyOption<TValue extends string> {
|
||||||
id: TValue;
|
id: TValue;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function PopoverTrigger({
|
|||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
className,
|
className,
|
||||||
align = "center",
|
align = "start",
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user