Basic crud working?

This commit is contained in:
Owen
2026-04-23 15:01:43 -07:00
parent fa117198a0
commit e7a9a19816
8 changed files with 80 additions and 104 deletions

View File

@@ -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."
} }

View File

@@ -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) {

View File

@@ -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(),

View File

@@ -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}

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
} }