Show/allow unicode domain name

This commit is contained in:
Pallavi
2025-08-08 23:08:08 +05:30
parent f37eda4739
commit 78f464f6ca
23 changed files with 333 additions and 173 deletions

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting to login...", "autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error", "autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"internationaldomaindetected": "International Domain Detected",
"willbestoredas": "Will be stored as:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting to login...", "autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error", "autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
"internationaldomaindetected": "Detekována mezinárodní doména",
"willbestoredas": "Bude uloženo jako:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Weiterleitung zur Anmeldung...", "autoLoginRedirecting": "Weiterleitung zur Anmeldung...",
"autoLoginError": "Fehler bei der automatischen Anmeldung", "autoLoginError": "Fehler bei der automatischen Anmeldung",
"autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.", "autoLoginErrorNoRedirectUrl": "Keine Weiterleitungs-URL vom Identitätsanbieter erhalten.",
"autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL." "autoLoginErrorGeneratingUrl": "Fehler beim Generieren der Authentifizierungs-URL.",
"internationaldomaindetected": "Internationale Domäne erkannt",
"willbestoredas": "Wird gespeichert als:"
} }

View File

@@ -1494,5 +1494,7 @@
"documentation": "documentation" "documentation": "documentation"
}, },
"convertButton": "Convert This Node to Managed Self-Hosted" "convertButton": "Convert This Node to Managed Self-Hosted"
} },
"internationaldomaindetected": "International Domain Detected",
"willbestoredas": "Will be stored as:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirigiendo al inicio de sesión...", "autoLoginRedirecting": "Redirigiendo al inicio de sesión...",
"autoLoginError": "Error de inicio de sesión automático", "autoLoginError": "Error de inicio de sesión automático",
"autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.", "autoLoginErrorNoRedirectUrl": "No se recibió URL de redirección del proveedor de identidad.",
"autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación." "autoLoginErrorGeneratingUrl": "Error al generar URL de autenticación.",
"internationaldomaindetected": "Dominio internacional detectado",
"willbestoredas": "Se almacenará como: "
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirection vers la connexion...", "autoLoginRedirecting": "Redirection vers la connexion...",
"autoLoginError": "Erreur de connexion automatique", "autoLoginError": "Erreur de connexion automatique",
"autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.", "autoLoginErrorNoRedirectUrl": "Aucune URL de redirection reçue du fournisseur d'identité.",
"autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification." "autoLoginErrorGeneratingUrl": "Échec de la génération de l'URL d'authentification.",
"internationaldomaindetected": "Domaine international détecté",
"willbestoredas": "Sera stocké comme:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Reindirizzamento al login...", "autoLoginRedirecting": "Reindirizzamento al login...",
"autoLoginError": "Errore di Accesso Automatico", "autoLoginError": "Errore di Accesso Automatico",
"autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.", "autoLoginErrorNoRedirectUrl": "Nessun URL di reindirizzamento ricevuto dal provider di identità.",
"autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione." "autoLoginErrorGeneratingUrl": "Impossibile generare l'URL di autenticazione.",
"internationaldomaindetected": "Rilevato dominio internazionale",
"willbestoredas": "Verrà archiviato come:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "로그인으로 리디렉션 중...", "autoLoginRedirecting": "로그인으로 리디렉션 중...",
"autoLoginError": "자동 로그인 오류", "autoLoginError": "자동 로그인 오류",
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.", "autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패." "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
"internationaldomaindetected": "국제 도메인 감지됨",
"willbestoredas": "다음과 같이 저장됩니다."
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting naar inloggen...", "autoLoginRedirecting": "Redirecting naar inloggen...",
"autoLoginError": "Auto Login Fout", "autoLoginError": "Auto Login Fout",
"autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.", "autoLoginErrorNoRedirectUrl": "Geen redirect URL ontvangen van de identity provider.",
"autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt." "autoLoginErrorGeneratingUrl": "Genereren van authenticatie-URL mislukt.",
"internationaldomaindetected": "Internationaal Domein Gedetecteerd",
"willbestoredas": "Wordt opgeslagen als:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Przekierowanie do logowania...", "autoLoginRedirecting": "Przekierowanie do logowania...",
"autoLoginError": "Błąd automatycznego logowania", "autoLoginError": "Błąd automatycznego logowania",
"autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.", "autoLoginErrorNoRedirectUrl": "Nie otrzymano URL przekierowania od dostawcy tożsamości.",
"autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania." "autoLoginErrorGeneratingUrl": "Nie udało się wygenerować URL uwierzytelniania.",
"internationaldomaindetected": "Wykryto domenę międzynarodową",
"willbestoredas": "Będzie przechowywane jako:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecionando para login...", "autoLoginRedirecting": "Redirecionando para login...",
"autoLoginError": "Erro de Login Automático", "autoLoginError": "Erro de Login Automático",
"autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.", "autoLoginErrorNoRedirectUrl": "Nenhum URL de redirecionamento recebido do provedor de identidade.",
"autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação." "autoLoginErrorGeneratingUrl": "Falha ao gerar URL de autenticação.",
"internationaldomaindetected": "Domínio internacional detetado",
"willbestoredas": "Será armazenado como:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Перенаправление к входу...", "autoLoginRedirecting": "Перенаправление к входу...",
"autoLoginError": "Ошибка автоматического входа", "autoLoginError": "Ошибка автоматического входа",
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.", "autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации." "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
"internationaldomaindetected": "Обнаружен международный домен",
"willbestoredas": "Будет сохранен как:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...", "autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
"autoLoginError": "Otomatik Giriş Hatası", "autoLoginError": "Otomatik Giriş Hatası",
"autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.", "autoLoginErrorNoRedirectUrl": "Kimlik sağlayıcıdan yönlendirme URL'si alınamadı.",
"autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı." "autoLoginErrorGeneratingUrl": "Kimlik doğrulama URL'si oluşturulamadı.",
"internationaldomaindetected": "Uluslararası Etki Alanı Algılandı",
"willbestoredas": "Şu şekilde saklanacaktır:"
} }

View File

@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "重定向到登录...", "autoLoginRedirecting": "重定向到登录...",
"autoLoginError": "自动登录错误", "autoLoginError": "自动登录错误",
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。", "autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。" "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
"internationaldomaindetected": "检测到国际域名",
"willbestoredas": "将存储为:"
} }

View File

@@ -7,12 +7,13 @@ import {
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
FormMessage FormMessage,
FormDescription
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { useToast } from "@app/hooks/useToast"; import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react"; import { useState, useMemo } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -33,7 +34,7 @@ import { CreateDomainResponse } from "@server/routers/domain/createOrgDomain";
import { StrategySelect } from "@app/components/StrategySelect"; import { StrategySelect } from "@app/components/StrategySelect";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon, AlertTriangle } from "lucide-react"; import { InfoIcon, AlertTriangle, Globe } from "lucide-react";
import CopyToClipboard from "@app/components/CopyToClipboard"; import CopyToClipboard from "@app/components/CopyToClipboard";
import { import {
InfoSection, InfoSection,
@@ -43,9 +44,58 @@ import {
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { build } from "@server/build"; import { build } from "@server/build";
import { toASCII, toUnicode } from 'punycode';
// Helper functions for Unicode domain handling
function toPunycode(domain: string): string {
try {
const parts = toASCII(domain);
return parts;
} catch (error) {
return domain.toLowerCase();
}
}
function fromPunycode(domain: string): string {
try {
const parts = toUnicode(domain)
return parts;
} catch (error) {
return domain;
}
}
function isValidDomainFormat(domain: string): boolean {
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
if (!unicodeRegex.test(domain)) {
return false;
}
const parts = domain.split('.');
for (const part of parts) {
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
return false;
}
if (part.length > 63) {
return false;
}
}
if (domain.length > 253) {
return false;
}
return true;
}
const formSchema = z.object({ const formSchema = z.object({
baseDomain: z.string().min(1, "Domain is required"), baseDomain: z
.string()
.min(1, "Domain is required")
.refine((val) => isValidDomainFormat(val), "Invalid domain format")
.transform((val) => toPunycode(val)),
type: z.enum(["ns", "cname", "wildcard"]) type: z.enum(["ns", "cname", "wildcard"])
}); });
@@ -109,8 +159,14 @@ export default function CreateDomainForm({
} }
} }
const domainType = form.watch("type");
const baseDomain = form.watch("baseDomain"); const baseDomain = form.watch("baseDomain");
const domainInputValue = form.watch("baseDomain") || "";
const punycodePreview = useMemo(() => {
if (!domainInputValue) return "";
const punycode = toPunycode(domainInputValue);
return punycode !== domainInputValue.toLowerCase() ? punycode : "";
}, [domainInputValue]);
let domainOptions: any = []; let domainOptions: any = [];
if (build == "enterprise" || build == "saas") { if (build == "enterprise" || build == "saas") {
@@ -182,10 +238,23 @@ export default function CreateDomainForm({
<FormLabel>{t("domain")}</FormLabel> <FormLabel>{t("domain")}</FormLabel>
<FormControl> <FormControl>
<Input <Input
placeholder="example.com" placeholder="example.com, café.com, 日本.com"
{...field} {...field}
/> />
</FormControl> </FormControl>
{punycodePreview && (
<FormDescription className="flex items-center gap-2 text-xs">
<Alert>
<Globe className="h-4 w-4" />
<AlertTitle>{t("internationaldomaindetected")}</AlertTitle>
<AlertDescription>
<div className="mt-2 space-y-1">
<p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p>
</div>
</AlertDescription>
</Alert>
</FormDescription>
)}
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
@@ -206,66 +275,73 @@ export default function CreateDomainForm({
<div className="space-y-4"> <div className="space-y-4">
{createdDomain.nsRecords && {createdDomain.nsRecords &&
createdDomain.nsRecords.length > 0 && ( createdDomain.nsRecords.length > 0 && (
<div> <div>
<h3 className="font-medium mb-3"> <h3 className="font-medium mb-3">
{t("createDomainNsRecords")} {t("createDomainNsRecords")}
</h3> </h3>
<InfoSections cols={1}> <InfoSections cols={1}>
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("createDomainRecord")} {t("createDomainRecord")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{t( {t(
"createDomainType" "createDomainType"
)} )}
</span> </span>
<span className="text-sm font-mono"> <span className="text-sm font-mono">
NS NS
</span> </span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{t( {t(
"createDomainName" "createDomainName"
)} )}
</span> </span>
<span className="text-sm font-mono"> <div className="text-right">
{baseDomain} <span className="text-sm font-mono block">
</span> {fromPunycode(baseDomain)}
</div> </span>
<span className="text-sm font-medium"> {fromPunycode(baseDomain) !== baseDomain && (
{t( <span className="text-xs text-muted-foreground font-mono">
"createDomainValue" ({baseDomain})
)} </span>
</span> )}
{createdDomain.nsRecords.map(
(
nsRecord,
index
) => (
<div
className="flex justify-between items-center"
key={index}
>
<CopyToClipboard
text={
nsRecord
}
/>
</div> </div>
) </div>
)} <span className="text-sm font-medium">
</div> {t(
</InfoSectionContent> "createDomainValue"
</InfoSection> )}
</InfoSections> </span>
</div> {createdDomain.nsRecords.map(
)} (
nsRecord,
index
) => (
<div
className="flex justify-between items-center"
key={index}
>
<CopyToClipboard
text={
nsRecord
}
/>
</div>
)
)}
</div>
</InfoSectionContent>
</InfoSection>
</InfoSections>
</div>
)}
{createdDomain.cnameRecords && {createdDomain.cnameRecords &&
createdDomain.cnameRecords.length > 0 && ( createdDomain.cnameRecords.length > 0 && (
@@ -307,11 +383,16 @@ export default function CreateDomainForm({
"createDomainName" "createDomainName"
)} )}
</span> </span>
<span className="text-sm font-mono"> <div className="text-right">
{ <span className="text-sm font-mono block">
cnameRecord.baseDomain {fromPunycode(cnameRecord.baseDomain)}
} </span>
</span> {fromPunycode(cnameRecord.baseDomain) !== cnameRecord.baseDomain && (
<span className="text-xs text-muted-foreground font-mono">
({cnameRecord.baseDomain})
</span>
)}
</div>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
@@ -374,11 +455,16 @@ export default function CreateDomainForm({
"createDomainName" "createDomainName"
)} )}
</span> </span>
<span className="text-sm font-mono"> <div className="text-right">
{ <span className="text-sm font-mono block">
aRecord.baseDomain {fromPunycode(aRecord.baseDomain)}
} </span>
</span> {fromPunycode(aRecord.baseDomain) !== aRecord.baseDomain && (
<span className="text-xs text-muted-foreground font-mono">
({aRecord.baseDomain})
</span>
)}
</div>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
@@ -390,7 +476,7 @@ export default function CreateDomainForm({
{ {
aRecord.value aRecord.value
} }
</span> </span>
</div> </div>
</div> </div>
</InfoSectionContent> </InfoSectionContent>
@@ -440,11 +526,16 @@ export default function CreateDomainForm({
"createDomainName" "createDomainName"
)} )}
</span> </span>
<span className="text-sm font-mono"> <div className="text-right">
{ <span className="text-sm font-mono block">
txtRecord.baseDomain {fromPunycode(txtRecord.baseDomain)}
} </span>
</span> {fromPunycode(txtRecord.baseDomain) !== txtRecord.baseDomain && (
<span className="text-xs text-muted-foreground font-mono">
({txtRecord.baseDomain})
</span>
)}
</div>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-sm font-medium"> <span className="text-sm font-medium">
@@ -513,4 +604,4 @@ export default function CreateDomainForm({
</CredenzaContent> </CredenzaContent>
</Credenza> </Credenza>
); );
} }

View File

@@ -9,6 +9,7 @@ import { GetOrgResponse } from "@server/routers/org";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import OrgProvider from "@app/providers/OrgProvider"; import OrgProvider from "@app/providers/OrgProvider";
import { ListDomainsResponse } from "@server/routers/domain"; import { ListDomainsResponse } from "@server/routers/domain";
import { toUnicode } from 'punycode';
type Props = { type Props = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -22,7 +23,13 @@ export default async function DomainsPage(props: Props) {
const res = await internal.get< const res = await internal.get<
AxiosResponse<ListDomainsResponse> AxiosResponse<ListDomainsResponse>
>(`/org/${params.orgId}/domains`, await authCookieHeader()); >(`/org/${params.orgId}/domains`, await authCookieHeader());
domains = res.data.data.domains as DomainRow[];
const rawDomains = res.data.data.domains as DomainRow[];
domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain),
}));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@@ -9,6 +9,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@/components/ui/select"; } from "@/components/ui/select";
import { toUnicode } from "punycode";
interface DomainOption { interface DomainOption {
baseDomain: string; baseDomain: string;
@@ -91,7 +92,7 @@ export default function CustomDomainInput({
key={option.domainId} key={option.domainId}
value={option.domainId} value={option.domainId}
> >
.{option.baseDomain} .{toUnicode(option.baseDomain)}
</SelectItem> </SelectItem>
))} ))}
</SelectContent> </SelectContent>

View File

@@ -12,15 +12,19 @@ import {
} from "@app/components/InfoSection"; } from "@app/components/InfoSection";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
import { toUnicode } from 'punycode';
type ResourceInfoBoxType = {}; type ResourceInfoBoxType = {};
export default function ResourceInfoBox({}: ResourceInfoBoxType) { export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext(); const { resource, authInfo } = useResourceContext();
const t = useTranslations(); const t = useTranslations();
const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
return ( return (
<Alert> <Alert>
@@ -34,9 +38,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{authInfo.password || {authInfo.password ||
authInfo.pincode || authInfo.pincode ||
authInfo.sso || authInfo.sso ||
authInfo.whitelist ? ( authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500"> <div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" /> <ShieldCheck className="w-4 h-4 mt-0.5" />
<span>{t("protected")}</span> <span>{t("protected")}</span>

View File

@@ -54,6 +54,8 @@ import DomainPicker from "@app/components/DomainPicker";
import { Globe } from "lucide-react"; import { Globe } from "lucide-react";
import { build } from "@server/build"; import { build } from "@server/build";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { DomainRow } from "../../../domains/DomainsTable";
import { toUnicode } from "punycode";
export default function GeneralForm() { export default function GeneralForm() {
const [formKey, setFormKey] = useState(0); const [formKey, setFormKey] = useState(0);
@@ -155,7 +157,11 @@ export default function GeneralForm() {
}); });
if (res?.status === 200) { if (res?.status === 200) {
const domains = res.data.data.domains; const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain),
}));
setBaseDomains(domains); setBaseDomains(domains);
setFormKey((key) => key + 1); setFormKey((key) => key + 1);
} }
@@ -319,10 +325,10 @@ export default function GeneralForm() {
.target .target
.value .value
? parseInt( ? parseInt(
e e
.target .target
.value .value
) )
: undefined : undefined
) )
} }

View File

@@ -89,6 +89,8 @@ import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target"; import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker"; import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget"; import { parseHostTarget } from "@app/lib/parseHostTarget";
import { toUnicode } from 'punycode';
import { DomainRow } from "../../domains/DomainsTable";
const baseResourceFormSchema = z.object({ const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
@@ -469,7 +471,11 @@ export default function Page() {
}); });
if (res?.status === 200) { if (res?.status === 200) {
const domains = res.data.data.domains; const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain),
}));
setBaseDomains(domains); setBaseDomains(domains);
// if (domains.length) { // if (domains.length) {
// httpForm.setValue("domainId", domains[0].domainId); // httpForm.setValue("domainId", domains[0].domainId);

View File

@@ -14,6 +14,7 @@ import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider"; import OrgProvider from "@app/providers/OrgProvider";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv"; import { pullEnv } from "@app/lib/pullEnv";
import { toUnicode } from "punycode";
type ResourcesPageProps = { type ResourcesPageProps = {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -75,7 +76,9 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
id: resource.resourceId, id: resource.resourceId,
name: resource.name, name: resource.name,
orgId: params.orgId, orgId: params.orgId,
domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`,
protocol: resource.protocol, protocol: resource.protocol,
proxyPort: resource.proxyPort, proxyPort: resource.proxyPort,
http: resource.http, http: resource.http,

View File

@@ -67,6 +67,7 @@ import {
} from "@app/components/ui/collapsible"; } from "@app/components/ui/collapsible";
import AccessTokenSection from "./AccessTokenUsage"; import AccessTokenSection from "./AccessTokenUsage";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { toUnicode } from 'punycode';
type FormProps = { type FormProps = {
open: boolean; open: boolean;
@@ -159,7 +160,7 @@ export default function CreateShareLinkForm({
.map((r) => ({ .map((r) => ({
resourceId: r.resourceId, resourceId: r.resourceId,
name: r.name, name: r.name,
resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/` resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
})) }))
); );
} }

View File

@@ -43,6 +43,7 @@ import {
validateByDomainType, validateByDomainType,
isValidSubdomainStructure isValidSubdomainStructure
} from "@/lib/subdomain-utils"; } from "@/lib/subdomain-utils";
import { toUnicode } from "punycode";
type OrganizationDomain = { type OrganizationDomain = {
domainId: string; domainId: string;
@@ -126,6 +127,7 @@ export default function DomainPicker2({
) )
.map((domain) => ({ .map((domain) => ({
...domain, ...domain,
baseDomain: toUnicode(domain.baseDomain),
type: domain.type as "ns" | "cname" | "wildcard" type: domain.type as "ns" | "cname" | "wildcard"
})); }));
setOrganizationDomains(domains); setOrganizationDomains(domains);
@@ -406,6 +408,12 @@ export default function DomainPicker2({
const hasMoreProvided = const hasMoreProvided =
sortedAvailableOptions.length > providedDomainsShown; sortedAvailableOptions.length > providedDomainsShown;
const isValidDomainCharacter = (char: string) => {
// Allow Unicode letters, numbers, hyphens, and periods
return /[\p{L}\p{N}.-]/u.test(char);
};
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -424,8 +432,8 @@ export default function DomainPicker2({
showProvidedDomainSearch showProvidedDomainSearch
? "" ? ""
: showSubdomainInput : showSubdomainInput
? "" ? ""
: t("domainPickerNotAvailableForCname") : t("domainPickerNotAvailableForCname")
} }
disabled={ disabled={
!showSubdomainInput && !showProvidedDomainSearch !showSubdomainInput && !showProvidedDomainSearch
@@ -436,10 +444,16 @@ export default function DomainPicker2({
"border-red-500 focus:border-red-500" "border-red-500 focus:border-red-500"
)} )}
onChange={(e) => { onChange={(e) => {
const rawInput = e.target.value;
const validInput = rawInput
.split("")
.filter((char) => isValidDomainCharacter(char))
.join("");
if (showProvidedDomainSearch) { if (showProvidedDomainSearch) {
handleProvidedDomainInputChange(e.target.value); handleProvidedDomainInputChange(validInput);
} else { } else {
handleSubdomainChange(e.target.value); handleSubdomainChange(validInput);
} }
}} }}
/> />
@@ -448,7 +462,6 @@ export default function DomainPicker2({
This subdomain contains invalid characters or structure. It will be sanitized automatically when you save. This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.
</p> </p>
)} )}
{showSubdomainInput && !subdomainInput && ( {showSubdomainInput && !subdomainInput && (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("domainPickerEnterSubdomainOrLeaveBlank")} {t("domainPickerEnterSubdomainOrLeaveBlank")}
@@ -474,7 +487,7 @@ export default function DomainPicker2({
{selectedBaseDomain ? ( {selectedBaseDomain ? (
<div className="flex items-center space-x-2 min-w-0 flex-1"> <div className="flex items-center space-x-2 min-w-0 flex-1">
{selectedBaseDomain.type === {selectedBaseDomain.type ===
"organization" ? null : ( "organization" ? null : (
<Zap className="h-4 w-4 flex-shrink-0" /> <Zap className="h-4 w-4 flex-shrink-0" />
)} )}
<span className="truncate"> <span className="truncate">
@@ -568,67 +581,67 @@ export default function DomainPicker2({
</CommandGroup> </CommandGroup>
{(build === "saas" || {(build === "saas" ||
build === "enterprise") && ( build === "enterprise") && (
<CommandSeparator className="my-2" /> <CommandSeparator className="my-2" />
)} )}
</> </>
)} )}
{(build === "saas" || {(build === "saas" ||
build === "enterprise") && ( build === "enterprise") && (
<CommandGroup <CommandGroup
heading={ heading={
build === "enterprise" build === "enterprise"
? t( ? t(
"domainPickerProvidedDomains" "domainPickerProvidedDomains"
) )
: t("domainPickerFreeDomains") : t("domainPickerFreeDomains")
} }
className="py-2" className="py-2"
> >
<CommandList> <CommandList>
<CommandItem <CommandItem
key="provided-search" key="provided-search"
onSelect={() => onSelect={() =>
handleBaseDomainSelect({ handleBaseDomainSelect({
id: "provided-search", id: "provided-search",
domain: domain:
build === build ===
"enterprise" "enterprise"
? "Provided Domain"
: "Free Provided Domain",
type: "provided-search"
})
}
className="mx-2 rounded-md"
>
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3">
<Zap className="h-4 w-4 text-primary" />
</div>
<div className="flex flex-col flex-1 min-w-0">
<span className="font-medium truncate">
{build === "enterprise"
? "Provided Domain" ? "Provided Domain"
: "Free Provided Domain", : "Free Provided Domain"}
type: "provided-search" </span>
}) <span className="text-xs text-muted-foreground">
} {t(
className="mx-2 rounded-md" "domainPickerSearchForAvailableDomains"
> )}
<div className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 mr-3"> </span>
<Zap className="h-4 w-4 text-primary" /> </div>
</div> <Check
<div className="flex flex-col flex-1 min-w-0"> className={cn(
<span className="font-medium truncate"> "h-4 w-4 text-primary",
{build === "enterprise" selectedBaseDomain?.id ===
? "Provided Domain" "provided-search"
: "Free Provided Domain"} ? "opacity-100"
</span> : "opacity-0"
<span className="text-xs text-muted-foreground">
{t(
"domainPickerSearchForAvailableDomains"
)} )}
</span> />
</div> </CommandItem>
<Check </CommandList>
className={cn( </CommandGroup>
"h-4 w-4 text-primary", )}
selectedBaseDomain?.id ===
"provided-search"
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
</CommandList>
</CommandGroup>
)}
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
@@ -684,7 +697,7 @@ export default function DomainPicker2({
htmlFor={option.domainNamespaceId} htmlFor={option.domainNamespaceId}
data-state={ data-state={
selectedProvidedDomain?.domainNamespaceId === selectedProvidedDomain?.domainNamespaceId ===
option.domainNamespaceId option.domainNamespaceId
? "checked" ? "checked"
: "unchecked" : "unchecked"
} }
@@ -764,4 +777,4 @@ function debounce<T extends (...args: any[]) => any>(
func(...args); func(...args);
}, wait); }, wait);
}; };
} }