all resources at the base domain closes #137

This commit is contained in:
Milo Schwartz
2025-02-03 21:18:16 -05:00
parent 0840c166ab
commit e475c1ea50
15 changed files with 496 additions and 141 deletions

View File

@@ -63,6 +63,8 @@ import { subdomainSchema } from "@server/schemas/subdomainSchema";
import Link from "next/link";
import { SquareArrowOutUpRight } from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Label } from "@app/components/ui/label";
const createResourceFormSchema = z
.object({
@@ -71,7 +73,8 @@ const createResourceFormSchema = z
siteId: z.number(),
http: z.boolean(),
protocol: z.string(),
proxyPort: z.number().optional()
proxyPort: z.number().optional(),
isBaseDomain: z.boolean().optional()
})
.refine(
(data) => {
@@ -92,7 +95,7 @@ const createResourceFormSchema = z
)
.refine(
(data) => {
if (data.http) {
if (data.http && !data.isBaseDomain) {
return subdomainSchema.safeParse(data.subdomain).success;
}
return true;
@@ -131,12 +134,15 @@ export default function CreateResourceForm({
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
const [showSnippets, setShowSnippets] = useState(false);
const [resourceId, setResourceId] = useState<number | null>(null);
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
"subdomain"
);
const form = useForm<CreateResourceFormValues>({
resolver: zodResolver(createResourceFormSchema),
defaultValues: {
subdomain: "",
name: "My Resource",
name: "",
http: true,
protocol: "tcp"
}
@@ -180,7 +186,8 @@ export default function CreateResourceForm({
http: data.http,
protocol: data.protocol,
proxyPort: data.http ? undefined : data.proxyPort,
siteId: data.siteId
siteId: data.siteId,
isBaseDomain: data.isBaseDomain
}
)
.catch((e) => {
@@ -246,7 +253,7 @@ export default function CreateResourceForm({
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Your name"
placeholder="Resource name"
{...field}
/>
</FormControl>
@@ -291,33 +298,89 @@ export default function CreateResourceForm({
/>
)}
{form.watch("http") &&
env.flags.allowBaseDomainResources && (
<div>
<RadioGroup
className="flex space-x-4"
defaultValue={domainType}
onValueChange={(val) => {
setDomainType(
val as any
);
form.setValue(
"isBaseDomain",
val === "basedomain"
);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="subdomain"
id="r1"
/>
<Label htmlFor="r1">
Subdomain
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="basedomain"
id="r2"
/>
<Label htmlFor="r2">
Base Domain
</Label>
</div>
</RadioGroup>
</div>
)}
{form.watch("http") && (
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
<FormLabel>
Subdomain
</FormLabel>
<FormControl>
<CustomDomainInput
value={
field.value ??
""
}
domainSuffix={
domainSuffix
}
placeholder="Enter subdomain"
onChange={(value) =>
form.setValue(
"subdomain",
{!env.flags
.allowBaseDomainResources && (
<FormLabel>
Subdomain
</FormLabel>
)}
{domainType ===
"subdomain" ? (
<FormControl>
<CustomDomainInput
value={
field.value ??
""
}
domainSuffix={
domainSuffix
}
placeholder="Subdomain"
onChange={(
value
)
}
/>
</FormControl>
) =>
form.setValue(
"subdomain",
value
)
}
/>
</FormControl>
) : (
<FormControl>
<Input
value={
domainSuffix
}
readOnly
disabled
/>
</FormControl>
)}
<FormDescription>
This is the fully
qualified domain name
@@ -471,9 +534,7 @@ export default function CreateResourceForm({
site
) => (
<CommandItem
value={
`${site.siteId}:${site.name}:${site.niceId}`
}
value={`${site.siteId}:${site.name}:${site.niceId}`}
key={
site.siteId
}
@@ -567,21 +628,25 @@ export default function CreateResourceForm({
)}
</CredenzaBody>
<CredenzaFooter>
{!showSnippets && <Button
type="submit"
form="create-resource-form"
loading={loading}
disabled={loading}
>
Create Resource
</Button>}
{!showSnippets && (
<Button
type="submit"
form="create-resource-form"
loading={loading}
disabled={loading}
>
Create Resource
</Button>
)}
{showSnippets && <Button
loading={loading}
onClick={() => goToResource()}
>
Go to Resource
</Button>}
{showSnippets && (
<Button
loading={loading}
onClick={() => goToResource()}
>
Go to Resource
</Button>
)}
<CredenzaClose asChild>
<Button variant="outline">Close</Button>

View File

@@ -2,11 +2,7 @@
import { useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
InfoIcon,
ShieldCheck,
ShieldOff
} from "lucide-react";
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { useResourceContext } from "@app/hooks/useResourceContext";
import { Separator } from "@app/components/ui/separator";
@@ -26,9 +22,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { org } = useOrgContext();
const { resource, authInfo } = useResourceContext();
const fullUrl = `${resource.ssl ? "https" : "http"}://${
resource.subdomain
}.${org.org.domain}`;
let fullUrl = `${resource.ssl ? "https" : "http"}://`;
if (resource.isBaseDomain) {
fullUrl = fullUrl + org.org.domain;
} else {
fullUrl = fullUrl + `${resource.subdomain}.${org.org.domain}`;
}
return (
<Alert>
@@ -82,7 +81,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
<InfoSection>
<InfoSectionTitle>Protocol</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.protocol.toUpperCase()}</span>
<span>
{resource.protocol.toUpperCase()}
</span>
</InfoSectionContent>
</InfoSection>
<Separator orientation="vertical" />

View File

@@ -132,9 +132,8 @@ export default function ReverseProxyTargets(props: {
defaultValues: {
ip: "",
method: resource.http ? "http" : null,
port: ""
// protocol: "TCP",
}
} as z.infer<typeof addTargetSchema>
});
useEffect(() => {

View File

@@ -51,13 +51,17 @@ import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { subdomainSchema } from "@server/schemas/subdomainSchema";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { pullEnv } from "@app/lib/pullEnv";
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
import { Label } from "@app/components/ui/label";
const GeneralFormSchema = z
.object({
subdomain: z.string().optional(),
name: z.string().min(1).max(255),
proxyPort: z.number().optional(),
http: z.boolean()
http: z.boolean(),
isBaseDomain: z.boolean().optional()
})
.refine(
(data) => {
@@ -78,7 +82,7 @@ const GeneralFormSchema = z
)
.refine(
(data) => {
if (data.http) {
if (data.http && !data.isBaseDomain) {
return subdomainSchema.safeParse(data.subdomain).success;
}
return true;
@@ -103,9 +107,11 @@ export default function GeneralForm() {
const { org } = useOrgContext();
const router = useRouter();
const { env } = useEnvContext();
const orgId = params.orgId;
const api = createApiClient(useEnvContext());
const api = createApiClient({ env });
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [saveLoading, setSaveLoading] = useState(false);
@@ -113,13 +119,18 @@ export default function GeneralForm() {
const [transferLoading, setTransferLoading] = useState(false);
const [open, setOpen] = useState(false);
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
resource.isBaseDomain ? "basedomain" : "subdomain"
);
const form = useForm<GeneralFormValues>({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: resource.name,
subdomain: resource.subdomain ? resource.subdomain : undefined,
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
http: resource.http
http: resource.http,
isBaseDomain: resource.isBaseDomain ? true : false
},
mode: "onChange"
});
@@ -148,7 +159,8 @@ export default function GeneralForm() {
.post(`resource/${resource?.resourceId}`, {
name: data.name,
subdomain: data.subdomain,
proxyPort: data.proxyPort
proxyPort: data.proxyPort,
isBaseDomain: data.isBaseDomain
})
.catch((e) => {
toast({
@@ -170,7 +182,8 @@ export default function GeneralForm() {
updateResource({
name: data.name,
subdomain: data.subdomain,
proxyPort: data.proxyPort
proxyPort: data.proxyPort,
isBaseDomain: data.isBaseDomain
});
}
setSaveLoading(false);
@@ -242,40 +255,103 @@ export default function GeneralForm() {
)}
/>
{resource.http ? (
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
<FormLabel>Subdomain</FormLabel>
<FormControl>
<CustomDomainInput
value={
field.value || ""
}
domainSuffix={
domainSuffix
}
placeholder="Enter subdomain"
onChange={(value) =>
form.setValue(
"subdomain",
value
)
}
/>
</FormControl>
<FormDescription>
This is the subdomain that
will be used to access the
resource.
</FormDescription>
<FormMessage />
</FormItem>
{resource.http && (
<>
{env.flags.allowBaseDomainResources && (
<div>
<RadioGroup
className="flex space-x-4"
defaultValue={domainType}
onValueChange={(val) => {
setDomainType(
val as any
);
form.setValue(
"isBaseDomain",
val === "basedomain"
);
}}
>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="subdomain"
id="r1"
/>
<Label htmlFor="r1">
Subdomain
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem
value="basedomain"
id="r2"
/>
<Label htmlFor="r2">
Base Domain
</Label>
</div>
</RadioGroup>
</div>
)}
/>
) : (
<FormField
control={form.control}
name="subdomain"
render={({ field }) => (
<FormItem>
{!env.flags
.allowBaseDomainResources && (
<FormLabel>
Subdomain
</FormLabel>
)}
{domainType ===
"subdomain" ? (
<FormControl>
<CustomDomainInput
value={
field.value ||
""
}
domainSuffix={
domainSuffix
}
placeholder="Enter subdomain"
onChange={(
value
) =>
form.setValue(
"subdomain",
value
)
}
/>
</FormControl>
) : (
<FormControl>
<Input
value={
domainSuffix
}
readOnly
disabled
/>
</FormControl>
)}
<FormDescription>
This is the subdomain
that will be used to
access the resource.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{!resource.http && (
<FormField
control={form.control}
name="proxyPort"

View File

@@ -1,30 +1,53 @@
"use client"
"use client";
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { Check } from "lucide-react";
import { cn } from "@app/lib/cn"
import { cn } from "@app/lib/cn";
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
export { Checkbox }
interface CheckboxWithLabelProps
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
label: string;
}
const CheckboxWithLabel = React.forwardRef<
React.ElementRef<typeof Checkbox>,
CheckboxWithLabelProps
>(({ className, label, id, ...props }, ref) => {
return (
<div className={cn("flex items-center space-x-2", className)}>
<Checkbox id={id} ref={ref} {...props} />
<label
htmlFor={id}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{label}
</label>
</div>
);
});
CheckboxWithLabel.displayName = "CheckboxWithLabel";
export { Checkbox, CheckboxWithLabel };

View File

@@ -6,8 +6,10 @@ export function pullEnv(): Env {
nextPort: process.env.NEXT_PORT as string,
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
resourceAccessTokenParam: process.env
.RESOURCE_ACCESS_TOKEN_PARAM as string,
resourceSessionRequestParam: process.env
.RESOURCE_SESSION_REQUEST_PARAM as string
},
app: {
environment: process.env.ENVIRONMENT as string,
@@ -29,6 +31,10 @@ export function pullEnv(): Env {
: false,
allowRawResources:
process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
allowBaseDomainResources:
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES === "true"
? true
: false
}
};
}

View File

@@ -18,5 +18,6 @@ export type Env = {
disableUserCreateOrg: boolean;
emailVerificationRequired: boolean;
allowRawResources: boolean;
allowBaseDomainResources: boolean;
}
};