mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-28 11:43:03 +00:00
Basic crud working?
This commit is contained in:
@@ -3175,5 +3175,6 @@
|
|||||||
"webhookUrlLabel": "URL",
|
"webhookUrlLabel": "URL",
|
||||||
"webhookHeaderKeyPlaceholder": "Key",
|
"webhookHeaderKeyPlaceholder": "Key",
|
||||||
"webhookHeaderValuePlaceholder": "Value",
|
"webhookHeaderValuePlaceholder": "Value",
|
||||||
"alertLabel": "Alert"
|
"alertLabel": "Alert",
|
||||||
|
"domainPickerWildcardSubdomainNotAllowed": "Wildcard subdomains are not allowed."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export async function validateAndConstructDomain(
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fullDomain,
|
fullDomain,
|
||||||
subdomain: isWildcard ? "*" : (finalSubdomain ?? null),
|
subdomain: finalSubdomain ?? null,
|
||||||
wildcard: isWildcard
|
wildcard: isWildcard
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const updateHttpResourceBodySchema = z
|
|||||||
"niceId can only contain letters, numbers, and dashes"
|
"niceId can only contain letters, numbers, and dashes"
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
subdomain: subdomainSchema.nullable().optional(),
|
subdomain: z.string().nullable().optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
sso: z.boolean().optional(),
|
sso: z.boolean().optional(),
|
||||||
blockAccess: z.boolean().optional(),
|
blockAccess: z.boolean().optional(),
|
||||||
|
|||||||
@@ -670,6 +670,7 @@ export default function GeneralForm() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div id="resource-domain-picker">
|
<div id="resource-domain-picker">
|
||||||
<DomainPicker
|
<DomainPicker
|
||||||
|
allowWildcard={true}
|
||||||
key={resource.resourceId}
|
key={resource.resourceId}
|
||||||
orgId={orgId as string}
|
orgId={orgId as string}
|
||||||
cols={2}
|
cols={2}
|
||||||
|
|||||||
@@ -1132,6 +1132,7 @@ export default function Page() {
|
|||||||
<SettingsSectionBody>
|
<SettingsSectionBody>
|
||||||
<SettingsSectionForm>
|
<SettingsSectionForm>
|
||||||
<DomainPicker
|
<DomainPicker
|
||||||
|
allowWildcard={true}
|
||||||
orgId={orgId as string}
|
orgId={orgId as string}
|
||||||
warnOnProvidedDomain={
|
warnOnProvidedDomain={
|
||||||
remoteExitNodes.length >= 1
|
remoteExitNodes.length >= 1
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
Zap
|
Zap
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
import { usePaidStatus } from "@/hooks/usePaidStatus";
|
||||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { toUnicode } from "punycode";
|
import { toUnicode } from "punycode";
|
||||||
@@ -119,11 +120,8 @@ export default function DomainPicker({
|
|||||||
orgQueries.domains({ orgId })
|
orgQueries.domains({ orgId })
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wildcard mode toggle — only relevant when allowWildcard is true, the
|
// Wildcard mode is derived from the input itself — if the user types a
|
||||||
// user has a paid plan, and the selected base domain supports it.
|
// wildcard subdomain (e.g. *.foo) and allowWildcard is enabled, it's active.
|
||||||
const [wildcardMode, setWildcardMode] = useState(
|
|
||||||
wildcardAllowed && !!defaultSubdomain && isWildcardSubdomain(defaultSubdomain)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!env.flags.usePangolinDns) {
|
if (!env.flags.usePangolinDns) {
|
||||||
hideFreeDomain = true;
|
hideFreeDomain = true;
|
||||||
@@ -193,7 +191,8 @@ export default function DomainPicker({
|
|||||||
firstOrExistingDomain.type !== "cname"
|
firstOrExistingDomain.type !== "cname"
|
||||||
? defaultSubdomain?.trim() || undefined
|
? defaultSubdomain?.trim() || undefined
|
||||||
: undefined;
|
: undefined;
|
||||||
const isWc = allowWildcard && !!sub && isWildcardSubdomain(sub);
|
const isWc =
|
||||||
|
allowWildcard && !!sub && isWildcardSubdomain(sub);
|
||||||
|
|
||||||
onDomainChange?.({
|
onDomainChange?.({
|
||||||
domainId: firstOrExistingDomain.domainId,
|
domainId: firstOrExistingDomain.domainId,
|
||||||
@@ -300,7 +299,8 @@ export default function DomainPicker({
|
|||||||
}, [userInput, debouncedCheckAvailability, selectedBaseDomain]);
|
}, [userInput, debouncedCheckAvailability, selectedBaseDomain]);
|
||||||
|
|
||||||
const finalizeSubdomain = (sub: string, base: DomainOption): string => {
|
const finalizeSubdomain = (sub: string, base: DomainOption): string => {
|
||||||
const sanitized = finalizeSubdomainSanitize(sub, wildcardAllowed && wildcardMode);
|
const wildcardMode = wildcardAllowed && isWildcardSubdomain(sub);
|
||||||
|
const sanitized = finalizeSubdomainSanitize(sub, wildcardMode);
|
||||||
|
|
||||||
if (!sanitized) {
|
if (!sanitized) {
|
||||||
toast({
|
toast({
|
||||||
@@ -317,7 +317,7 @@ export default function DomainPicker({
|
|||||||
? "provided-search"
|
? "provided-search"
|
||||||
: "organization",
|
: "organization",
|
||||||
domainType: base.domainType,
|
domainType: base.domainType,
|
||||||
allowWildcard: wildcardAllowed && wildcardMode
|
allowWildcard: wildcardMode
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -346,7 +346,7 @@ export default function DomainPicker({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubdomainChange = (value: string) => {
|
const handleSubdomainChange = (value: string) => {
|
||||||
const raw = sanitizeInputRaw(value, wildcardAllowed && wildcardMode);
|
const raw = sanitizeInputRaw(value, allowWildcard);
|
||||||
setSubdomainInput(raw);
|
setSubdomainInput(raw);
|
||||||
setSelectedProvidedDomain(null);
|
setSelectedProvidedDomain(null);
|
||||||
|
|
||||||
@@ -354,7 +354,7 @@ export default function DomainPicker({
|
|||||||
const fullDomain = raw
|
const fullDomain = raw
|
||||||
? `${raw}.${selectedBaseDomain.domain}`
|
? `${raw}.${selectedBaseDomain.domain}`
|
||||||
: selectedBaseDomain.domain;
|
: selectedBaseDomain.domain;
|
||||||
const isWc = wildcardAllowed && wildcardMode && isWildcardSubdomain(raw);
|
const isWc = wildcardAllowed && isWildcardSubdomain(raw);
|
||||||
|
|
||||||
onDomainChange?.({
|
onDomainChange?.({
|
||||||
domainId: selectedBaseDomain.domainId!,
|
domainId: selectedBaseDomain.domainId!,
|
||||||
@@ -384,16 +384,15 @@ export default function DomainPicker({
|
|||||||
const handleBaseDomainSelect = (option: DomainOption) => {
|
const handleBaseDomainSelect = (option: DomainOption) => {
|
||||||
let sub = subdomainInput;
|
let sub = subdomainInput;
|
||||||
|
|
||||||
// Wildcard mode is not applicable for cname or provided-search domains,
|
// If the selected domain doesn't support wildcards, strip any wildcard prefix.
|
||||||
// or when the user doesn't have a paid plan.
|
const supportsWildcard =
|
||||||
const newWildcardMode =
|
|
||||||
wildcardAllowed &&
|
wildcardAllowed &&
|
||||||
wildcardMode &&
|
|
||||||
option.type === "organization" &&
|
option.type === "organization" &&
|
||||||
option.domainType !== "cname";
|
option.domainType !== "cname";
|
||||||
|
|
||||||
if (newWildcardMode !== wildcardMode) {
|
if (!supportsWildcard && isWildcardSubdomain(sub)) {
|
||||||
setWildcardMode(newWildcardMode);
|
sub = sub.replace(/^\*\./, "");
|
||||||
|
setSubdomainInput(sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sub && sub.trim() !== "") {
|
if (sub && sub.trim() !== "") {
|
||||||
@@ -419,7 +418,7 @@ export default function DomainPicker({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fullDomain = sub ? `${sub}.${option.domain}` : option.domain;
|
const fullDomain = sub ? `${sub}.${option.domain}` : option.domain;
|
||||||
const isWc = newWildcardMode && !!sub && isWildcardSubdomain(sub);
|
const isWc = wildcardAllowed && !!sub && isWildcardSubdomain(sub);
|
||||||
|
|
||||||
if (option.type === "provided-search") {
|
if (option.type === "provided-search") {
|
||||||
onDomainChange?.(null); // prevent the modal from closing with `<subdomain>.Free Provided domain`
|
onDomainChange?.(null); // prevent the modal from closing with `<subdomain>.Free Provided domain`
|
||||||
@@ -464,7 +463,8 @@ export default function DomainPicker({
|
|||||||
? "provided-search"
|
? "provided-search"
|
||||||
: "organization",
|
: "organization",
|
||||||
domainType: selectedBaseDomain.domainType,
|
domainType: selectedBaseDomain.domainType,
|
||||||
allowWildcard: wildcardAllowed && wildcardMode
|
allowWildcard:
|
||||||
|
wildcardAllowed && isWildcardSubdomain(subdomainInput)
|
||||||
})
|
})
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
@@ -473,30 +473,6 @@ export default function DomainPicker({
|
|||||||
selectedBaseDomain.type === "organization" &&
|
selectedBaseDomain.type === "organization" &&
|
||||||
selectedBaseDomain.domainType !== "cname";
|
selectedBaseDomain.domainType !== "cname";
|
||||||
|
|
||||||
// Wildcard toggle is shown when the caller opts in and the selected domain
|
|
||||||
// supports it (ns or wildcard type). Shown even when unpaid so we can
|
|
||||||
// render a disabled state explaining it's a paid feature.
|
|
||||||
const showWildcardToggle =
|
|
||||||
allowWildcard &&
|
|
||||||
showSubdomainInput &&
|
|
||||||
(selectedBaseDomain?.domainType === "ns" ||
|
|
||||||
selectedBaseDomain?.domainType === "wildcard");
|
|
||||||
|
|
||||||
const handleWildcardModeChange = (enabled: boolean) => {
|
|
||||||
setWildcardMode(enabled);
|
|
||||||
// Reset subdomain input when toggling modes to avoid invalid state
|
|
||||||
setSubdomainInput("");
|
|
||||||
if (selectedBaseDomain?.type === "organization") {
|
|
||||||
onDomainChange?.({
|
|
||||||
domainId: selectedBaseDomain.domainId!,
|
|
||||||
type: "organization",
|
|
||||||
subdomain: undefined,
|
|
||||||
fullDomain: selectedBaseDomain.domain,
|
|
||||||
baseDomain: selectedBaseDomain.domain,
|
|
||||||
wildcard: enabled ? true : false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const showProvidedDomainSearch =
|
const showProvidedDomainSearch =
|
||||||
selectedBaseDomain?.type === "provided-search";
|
selectedBaseDomain?.type === "provided-search";
|
||||||
|
|
||||||
@@ -525,30 +501,6 @@ export default function DomainPicker({
|
|||||||
<Label htmlFor="subdomain-input">
|
<Label htmlFor="subdomain-input">
|
||||||
{t("domainPickerSubdomainLabel")}
|
{t("domainPickerSubdomainLabel")}
|
||||||
</Label>
|
</Label>
|
||||||
{showWildcardToggle && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
wildcardAllowed &&
|
|
||||||
handleWildcardModeChange(!wildcardMode)
|
|
||||||
}
|
|
||||||
title={
|
|
||||||
!wildcardAllowed
|
|
||||||
? t("domainPickerWildcardPaidOnly")
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
"text-xs px-2 py-0.5 rounded-full border transition-colors",
|
|
||||||
!wildcardAllowed
|
|
||||||
? "opacity-50 cursor-not-allowed border-input text-muted-foreground"
|
|
||||||
: wildcardMode
|
|
||||||
? "bg-primary text-primary-foreground border-primary"
|
|
||||||
: "bg-transparent text-muted-foreground border-input hover:border-primary hover:text-primary"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{t("domainPickerWildcard")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
id="subdomain-input"
|
id="subdomain-input"
|
||||||
@@ -561,8 +513,8 @@ export default function DomainPicker({
|
|||||||
showProvidedDomainSearch
|
showProvidedDomainSearch
|
||||||
? ""
|
? ""
|
||||||
: showSubdomainInput
|
: showSubdomainInput
|
||||||
? wildcardMode
|
? wildcardAllowed
|
||||||
? "*.level1"
|
? "*.level1 or level1"
|
||||||
: ""
|
: ""
|
||||||
: t("domainPickerNotAvailableForCname")
|
: t("domainPickerNotAvailableForCname")
|
||||||
}
|
}
|
||||||
@@ -584,11 +536,34 @@ export default function DomainPicker({
|
|||||||
/>
|
/>
|
||||||
{showSubdomainInput &&
|
{showSubdomainInput &&
|
||||||
subdomainInput &&
|
subdomainInput &&
|
||||||
!isValidSubdomainStructure(subdomainInput, wildcardAllowed && wildcardMode) && (
|
!isValidSubdomainStructure(
|
||||||
|
subdomainInput,
|
||||||
|
wildcardAllowed &&
|
||||||
|
isWildcardSubdomain(subdomainInput)
|
||||||
|
) && (
|
||||||
<p className="text-sm text-red-500">
|
<p className="text-sm text-red-500">
|
||||||
{t("domainPickerInvalidSubdomainStructure")}
|
{t("domainPickerInvalidSubdomainStructure")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{allowWildcard &&
|
||||||
|
!wildcardAllowed &&
|
||||||
|
showSubdomainInput &&
|
||||||
|
isWildcardSubdomain(subdomainInput) && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-red-500">
|
||||||
|
{t(
|
||||||
|
"domainPickerWildcardSubdomainNotAllowed"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<PaidFeaturesAlert
|
||||||
|
tiers={
|
||||||
|
tierMatrix[
|
||||||
|
TierFeature.WildcardSubdomain
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -678,23 +653,23 @@ export default function DomainPicker({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{orgDomain.type ===
|
{orgDomain.type ===
|
||||||
"wildcard"
|
"wildcard" ? (
|
||||||
? t(
|
t(
|
||||||
"domainPickerManual"
|
"domainPickerManual"
|
||||||
)
|
)
|
||||||
: (
|
) : (
|
||||||
<>
|
<>
|
||||||
{orgDomain.type.toUpperCase()}{" "}
|
{orgDomain.type.toUpperCase()}{" "}
|
||||||
•{" "}
|
•{" "}
|
||||||
{orgDomain.verified
|
{orgDomain.verified
|
||||||
? t(
|
? t(
|
||||||
"domainPickerVerified"
|
"domainPickerVerified"
|
||||||
)
|
)
|
||||||
: t(
|
: t(
|
||||||
"domainPickerUnverified"
|
"domainPickerUnverified"
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Check
|
<Check
|
||||||
@@ -794,17 +769,17 @@ export default function DomainPicker({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{requiresPaywall && !hideFreeDomain && (
|
{requiresPaywall && !hideFreeDomain && (
|
||||||
<Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
<Card className="mt-3 border-black-500/30 bg-linear-to-br from-black-500/10 via-background to-background overflow-hidden">
|
||||||
<CardContent className="py-3 px-4">
|
<CardContent className="py-3 px-4">
|
||||||
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
<div className="flex items-center gap-2.5 text-sm text-muted-foreground">
|
||||||
<KeyRound className="size-4 shrink-0 text-black-500" />
|
<KeyRound className="size-4 shrink-0 text-black-500" />
|
||||||
<span>
|
<span>
|
||||||
{t("domainPickerFreeDomainsPaidFeature")}
|
{t("domainPickerFreeDomainsPaidFeature")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*showProvidedDomainSearch && build === "saas" && (
|
{/*showProvidedDomainSearch && build === "saas" && (
|
||||||
<Alert>
|
<Alert>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
} from "@app/components/ui/tooltip";
|
} from "@app/components/ui/tooltip";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
const TRIAL_DURATION_DAYS = 14;
|
const TRIAL_DURATION_DAYS = 10;
|
||||||
|
|
||||||
export default function ShowTrialCard({
|
export default function ShowTrialCard({
|
||||||
isCollapsed
|
isCollapsed
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ export function useSubscriptionStatusContext() {
|
|||||||
}
|
}
|
||||||
const context = useContext(SubscriptionStatusContext);
|
const context = useContext(SubscriptionStatusContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error(
|
return null;
|
||||||
"useSubscriptionStatusContext must be used within an SubscriptionStatusProvider"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user