diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 3717855e..e8c85487 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error",
"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:"
}
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index fc03108a..fe9530d3 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting to login...",
"autoLoginError": "Auto Login Error",
"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:"
}
diff --git a/messages/de-DE.json b/messages/de-DE.json
index 062b61af..c79a5f64 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Weiterleitung zur Anmeldung...",
"autoLoginError": "Fehler bei der automatischen Anmeldung",
"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:"
}
diff --git a/messages/en-US.json b/messages/en-US.json
index f9bb4f6b..dbfa817e 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -1494,5 +1494,7 @@
"documentation": "documentation"
},
"convertButton": "Convert This Node to Managed Self-Hosted"
- }
+ },
+ "internationaldomaindetected": "International Domain Detected",
+ "willbestoredas": "Will be stored as:"
}
diff --git a/messages/es-ES.json b/messages/es-ES.json
index 2b49d9bb..0bb67191 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirigiendo al inicio de sesión...",
"autoLoginError": "Error de inicio de sesión automático",
"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: "
}
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index ee08d77b..28bcce6a 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirection vers la connexion...",
"autoLoginError": "Erreur de connexion automatique",
"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:"
}
diff --git a/messages/it-IT.json b/messages/it-IT.json
index 83708597..b41079c3 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Reindirizzamento al login...",
"autoLoginError": "Errore di Accesso Automatico",
"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:"
}
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index b13dd19d..4b27bf55 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
"autoLoginError": "자동 로그인 오류",
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
- "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패."
+ "autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
+ "internationaldomaindetected": "국제 도메인 감지됨",
+ "willbestoredas": "다음과 같이 저장됩니다."
}
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index 76c28cb5..91e0be86 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecting naar inloggen...",
"autoLoginError": "Auto Login Fout",
"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:"
}
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index be33709c..9491a94d 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Przekierowanie do logowania...",
"autoLoginError": "Błąd automatycznego logowania",
"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:"
}
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index 4efd0237..050280fa 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Redirecionando para login...",
"autoLoginError": "Erro de Login Automático",
"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:"
}
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index ed44702d..eef9aad6 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Перенаправление к входу...",
"autoLoginError": "Ошибка автоматического входа",
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
- "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации."
+ "autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
+ "internationaldomaindetected": "Обнаружен международный домен",
+ "willbestoredas": "Будет сохранен как:"
}
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index 89de6876..09115e9a 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "Girişe yönlendiriliyorsunuz...",
"autoLoginError": "Otomatik Giriş Hatası",
"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:"
}
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 06cd8549..dee4cff1 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -1457,5 +1457,7 @@
"autoLoginRedirecting": "重定向到登录...",
"autoLoginError": "自动登录错误",
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
- "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。"
+ "autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
+ "internationaldomaindetected": "检测到国际域名",
+ "willbestoredas": "将存储为:"
}
diff --git a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx b/src/app/[orgId]/settings/domains/CreateDomainForm.tsx
index 31bf82f1..e609a8ac 100644
--- a/src/app/[orgId]/settings/domains/CreateDomainForm.tsx
+++ b/src/app/[orgId]/settings/domains/CreateDomainForm.tsx
@@ -7,12 +7,13 @@ import {
FormField,
FormItem,
FormLabel,
- FormMessage
+ FormMessage,
+ FormDescription
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useToast } from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod";
-import { useState } from "react";
+import { useState, useMemo } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import {
@@ -33,7 +34,7 @@ import { CreateDomainResponse } from "@server/routers/domain/createOrgDomain";
import { StrategySelect } from "@app/components/StrategySelect";
import { AxiosResponse } from "axios";
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 {
InfoSection,
@@ -43,9 +44,58 @@ import {
} from "@app/components/InfoSection";
import { useOrgContext } from "@app/hooks/useOrgContext";
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({
- 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"])
});
@@ -109,8 +159,14 @@ export default function CreateDomainForm({
}
}
- const domainType = form.watch("type");
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 = [];
if (build == "enterprise" || build == "saas") {
@@ -182,10 +238,23 @@ export default function CreateDomainForm({
{t("domain")}
+ {punycodePreview && (
+
+
+
+ {t("internationaldomaindetected")}
+
+
+
{t("willbestoredas")} {punycodePreview}
+
+
+
+
+ )}
)}
@@ -206,66 +275,73 @@ export default function CreateDomainForm({
{createdDomain.nsRecords &&
- createdDomain.nsRecords.length > 0 && (
-
-
- {t("createDomainNsRecords")}
-
-
-
-
- {t("createDomainRecord")}
-
-
-
-
-
- {t(
- "createDomainType"
- )}
-
-
- NS
-
-
-
-
- {t(
- "createDomainName"
- )}
-
-
- {baseDomain}
-
-
-
- {t(
- "createDomainValue"
- )}
-
- {createdDomain.nsRecords.map(
- (
- nsRecord,
- index
- ) => (
-
-
+ createdDomain.nsRecords.length > 0 && (
+
+
+ {t("createDomainNsRecords")}
+
+
+
+
+ {t("createDomainRecord")}
+
+
+
+
+
+ {t(
+ "createDomainType"
+ )}
+
+
+ NS
+
+
+
+
+ {t(
+ "createDomainName"
+ )}
+
+
+
+ {fromPunycode(baseDomain)}
+
+ {fromPunycode(baseDomain) !== baseDomain && (
+
+ ({baseDomain})
+
+ )}
- )
- )}
-
-
-
-
-
- )}
+
+
+ {t(
+ "createDomainValue"
+ )}
+
+ {createdDomain.nsRecords.map(
+ (
+ nsRecord,
+ index
+ ) => (
+
+
+
+ )
+ )}
+
+
+
+
+
+ )}
{createdDomain.cnameRecords &&
createdDomain.cnameRecords.length > 0 && (
@@ -307,11 +383,16 @@ export default function CreateDomainForm({
"createDomainName"
)}
-
- {
- cnameRecord.baseDomain
- }
-
+
+
+ {fromPunycode(cnameRecord.baseDomain)}
+
+ {fromPunycode(cnameRecord.baseDomain) !== cnameRecord.baseDomain && (
+
+ ({cnameRecord.baseDomain})
+
+ )}
+
@@ -374,11 +455,16 @@ export default function CreateDomainForm({
"createDomainName"
)}
-
- {
- aRecord.baseDomain
- }
-
+
+
+ {fromPunycode(aRecord.baseDomain)}
+
+ {fromPunycode(aRecord.baseDomain) !== aRecord.baseDomain && (
+
+ ({aRecord.baseDomain})
+
+ )}
+
@@ -390,7 +476,7 @@ export default function CreateDomainForm({
{
aRecord.value
}
-
+
@@ -440,11 +526,16 @@ export default function CreateDomainForm({
"createDomainName"
)}
-
- {
- txtRecord.baseDomain
- }
-
+
+
+ {fromPunycode(txtRecord.baseDomain)}
+
+ {fromPunycode(txtRecord.baseDomain) !== txtRecord.baseDomain && (
+
+ ({txtRecord.baseDomain})
+
+ )}
+
@@ -513,4 +604,4 @@ export default function CreateDomainForm({
);
-}
+}
\ No newline at end of file
diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx
index d20e431f..c85fe10d 100644
--- a/src/app/[orgId]/settings/domains/page.tsx
+++ b/src/app/[orgId]/settings/domains/page.tsx
@@ -9,6 +9,7 @@ import { GetOrgResponse } from "@server/routers/org";
import { redirect } from "next/navigation";
import OrgProvider from "@app/providers/OrgProvider";
import { ListDomainsResponse } from "@server/routers/domain";
+import { toUnicode } from 'punycode';
type Props = {
params: Promise<{ orgId: string }>;
@@ -22,7 +23,13 @@ export default async function DomainsPage(props: Props) {
const res = await internal.get<
AxiosResponse
>(`/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) {
console.error(e);
}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx
index 0764d740..171f5683 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx
+++ b/src/app/[orgId]/settings/resources/[resourceId]/CustomDomainInput.tsx
@@ -9,6 +9,7 @@ import {
SelectTrigger,
SelectValue
} from "@/components/ui/select";
+import { toUnicode } from "punycode";
interface DomainOption {
baseDomain: string;
@@ -91,7 +92,7 @@ export default function CustomDomainInput({
key={option.domainId}
value={option.domainId}
>
- .{option.baseDomain}
+ .{toUnicode(option.baseDomain)}
))}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
index af7d96fc..8da95ec0 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
+++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx
@@ -12,15 +12,19 @@ import {
} from "@app/components/InfoSection";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
+import { toUnicode } from 'punycode';
type ResourceInfoBoxType = {};
-export default function ResourceInfoBox({}: ResourceInfoBoxType) {
+export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext();
const t = useTranslations();
- const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
+
+ const fullUrl = `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`;
+
+
return (
@@ -34,9 +38,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
{authInfo.password ||
- authInfo.pincode ||
- authInfo.sso ||
- authInfo.whitelist ? (
+ authInfo.pincode ||
+ authInfo.sso ||
+ authInfo.whitelist ? (
{t("protected")}
diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
index b95ecef2..ce8f29a7 100644
--- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
+++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx
@@ -54,6 +54,8 @@ import DomainPicker from "@app/components/DomainPicker";
import { Globe } from "lucide-react";
import { build } from "@server/build";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
+import { DomainRow } from "../../../domains/DomainsTable";
+import { toASCII, toUnicode } from "punycode";
export default function GeneralForm() {
const [formKey, setFormKey] = useState(0);
@@ -80,7 +82,7 @@ export default function GeneralForm() {
const [loadingPage, setLoadingPage] = useState(true);
const [resourceFullDomain, setResourceFullDomain] = useState(
- `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`
+ `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
);
const [selectedDomain, setSelectedDomain] = useState<{
domainId: string;
@@ -155,7 +157,11 @@ export default function GeneralForm() {
});
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);
setFormKey((key) => key + 1);
}
@@ -180,7 +186,7 @@ export default function GeneralForm() {
{
enabled: data.enabled,
name: data.name,
- subdomain: data.subdomain,
+ subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
domainId: data.domainId,
proxyPort: data.proxyPort,
// ...(!resource.http && {
@@ -319,10 +325,10 @@ export default function GeneralForm() {
.target
.value
? parseInt(
- e
- .target
- .value
- )
+ e
+ .target
+ .value
+ )
: undefined
)
}
@@ -472,7 +478,6 @@ export default function GeneralForm() {
setEditDomainOpen(false);
toast({
- title: "Domain sanitized",
description: `Final domain: ${sanitizedFullDomain}`,
});
}
diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx
index 9caa3655..782b3135 100644
--- a/src/app/[orgId]/settings/resources/create/page.tsx
+++ b/src/app/[orgId]/settings/resources/create/page.tsx
@@ -89,6 +89,8 @@ import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
+import { toASCII, toUnicode } from 'punycode';
+import { DomainRow } from "../../domains/DomainsTable";
const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255),
@@ -327,7 +329,7 @@ export default function Page() {
if (isHttp) {
const httpData = httpForm.getValues();
Object.assign(payload, {
- subdomain: httpData.subdomain,
+ subdomain: httpData.subdomain ? toASCII(httpData.subdomain) : undefined,
domainId: httpData.domainId,
protocol: "tcp"
});
@@ -469,7 +471,11 @@ export default function Page() {
});
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);
// if (domains.length) {
// httpForm.setValue("domainId", domains[0].domainId);
diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx
index d5af500b..f8ef5397 100644
--- a/src/app/[orgId]/settings/resources/page.tsx
+++ b/src/app/[orgId]/settings/resources/page.tsx
@@ -14,6 +14,7 @@ import { GetOrgResponse } from "@server/routers/org";
import OrgProvider from "@app/providers/OrgProvider";
import { getTranslations } from "next-intl/server";
import { pullEnv } from "@app/lib/pullEnv";
+import { toUnicode } from "punycode";
type ResourcesPageProps = {
params: Promise<{ orgId: string }>;
@@ -75,7 +76,9 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
id: resource.resourceId,
name: resource.name,
orgId: params.orgId,
- domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`,
+
+
+ domain: `${resource.ssl ? "https://" : "http://"}${toUnicode(resource.fullDomain || "")}`,
protocol: resource.protocol,
proxyPort: resource.proxyPort,
http: resource.http,
diff --git a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx
index 44891980..18c989ab 100644
--- a/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx
+++ b/src/app/[orgId]/settings/share-links/CreateShareLinkForm.tsx
@@ -67,6 +67,7 @@ import {
} from "@app/components/ui/collapsible";
import AccessTokenSection from "./AccessTokenUsage";
import { useTranslations } from "next-intl";
+import { toUnicode } from 'punycode';
type FormProps = {
open: boolean;
@@ -159,7 +160,7 @@ export default function CreateShareLinkForm({
.map((r) => ({
resourceId: r.resourceId,
name: r.name,
- resourceUrl: `${r.ssl ? "https://" : "http://"}${r.fullDomain}/`
+ resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/`
}))
);
}
diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx
index ab54ccce..f00292ee 100644
--- a/src/components/DomainPicker.tsx
+++ b/src/components/DomainPicker.tsx
@@ -43,6 +43,7 @@ import {
validateByDomainType,
isValidSubdomainStructure
} from "@/lib/subdomain-utils";
+import { toUnicode } from "punycode";
type OrganizationDomain = {
domainId: string;
@@ -126,6 +127,7 @@ export default function DomainPicker2({
)
.map((domain) => ({
...domain,
+ baseDomain: toUnicode(domain.baseDomain),
type: domain.type as "ns" | "cname" | "wildcard"
}));
setOrganizationDomains(domains);
@@ -334,8 +336,13 @@ export default function DomainPicker2({
const handleBaseDomainSelect = (option: DomainOption) => {
let sub = subdomainInput;
- sub = finalizeSubdomain(sub, option);
- setSubdomainInput(sub);
+ if (sub && sub.trim() !== "") {
+ sub = finalizeSubdomain(sub, option) || "";
+ setSubdomainInput(sub);
+ } else {
+ sub = "";
+ setSubdomainInput("");
+ }
if (option.type === "provided-search") {
setUserInput("");
@@ -406,6 +413,7 @@ export default function DomainPicker2({
const hasMoreProvided =
sortedAvailableOptions.length > providedDomainsShown;
+
return (
@@ -424,8 +432,8 @@ export default function DomainPicker2({
showProvidedDomainSearch
? ""
: showSubdomainInput
- ? ""
- : t("domainPickerNotAvailableForCname")
+ ? ""
+ : t("domainPickerNotAvailableForCname")
}
disabled={
!showSubdomainInput && !showProvidedDomainSearch
@@ -448,7 +456,6 @@ export default function DomainPicker2({
This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.
)}
-
{showSubdomainInput && !subdomainInput && (
{t("domainPickerEnterSubdomainOrLeaveBlank")}
@@ -474,7 +481,7 @@ export default function DomainPicker2({
{selectedBaseDomain ? (
{selectedBaseDomain.type ===
- "organization" ? null : (
+ "organization" ? null : (
)}
@@ -568,67 +575,67 @@ export default function DomainPicker2({
{(build === "saas" ||
build === "enterprise") && (
-
- )}
+
+ )}
>
)}
{(build === "saas" ||
build === "enterprise") && (
-
-
-
- handleBaseDomainSelect({
- id: "provided-search",
- domain:
- build ===
- "enterprise"
+
+
+
+ handleBaseDomainSelect({
+ id: "provided-search",
+ domain:
+ build ===
+ "enterprise"
+ ? "Provided Domain"
+ : "Free Provided Domain",
+ type: "provided-search"
+ })
+ }
+ className="mx-2 rounded-md"
+ >
+
+
+
+
+
+ {build === "enterprise"
? "Provided Domain"
- : "Free Provided Domain",
- type: "provided-search"
- })
- }
- className="mx-2 rounded-md"
- >
-
-
-
-
-
- {build === "enterprise"
- ? "Provided Domain"
- : "Free Provided Domain"}
-
-
- {t(
- "domainPickerSearchForAvailableDomains"
+ : "Free Provided Domain"}
+
+
+ {t(
+ "domainPickerSearchForAvailableDomains"
+ )}
+
+
+
-
-
-
-
-
- )}
+ />
+
+
+
+ )}
@@ -684,7 +691,7 @@ export default function DomainPicker2({
htmlFor={option.domainNamespaceId}
data-state={
selectedProvidedDomain?.domainNamespaceId ===
- option.domainNamespaceId
+ option.domainNamespaceId
? "checked"
: "unchecked"
}
@@ -764,4 +771,4 @@ function debounce any>(
func(...args);
}, wait);
};
-}
+}
\ No newline at end of file
diff --git a/src/lib/subdomain-utils.ts b/src/lib/subdomain-utils.ts
index 7c94db16..5a82e930 100644
--- a/src/lib/subdomain-utils.ts
+++ b/src/lib/subdomain-utils.ts
@@ -1,29 +1,32 @@
export type DomainType = "organization" | "provided" | "provided-search";
-export const SINGLE_LABEL_RE = /^[a-z0-9-]+$/i; // provided-search (no dots)
-export const MULTI_LABEL_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/i; // ns/wildcard
-export const SINGLE_LABEL_STRICT_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; // start/end alnum
+export const SINGLE_LABEL_RE = /^[\p{L}\p{N}-]+$/u; // provided-search (no dots)
+export const MULTI_LABEL_RE = /^[\p{L}\p{N}-]+(\.[\p{L}\p{N}-]+)*$/u; // ns/wildcard
+export const SINGLE_LABEL_STRICT_RE = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u; // start/end alnum
export function sanitizeInputRaw(input: string): string {
if (!input) return "";
- return input.toLowerCase().replace(/[^a-z0-9.-]/g, "");
+ return input
+ .toLowerCase()
+ .normalize("NFC") // normalize Unicode
+ .replace(/[^\p{L}\p{N}.-]/gu, ""); // allow Unicode letters, numbers, dot, hyphen
}
export function finalizeSubdomainSanitize(input: string): string {
if (!input) return "";
return input
.toLowerCase()
- .replace(/[^a-z0-9.-]/g, "") // allow only valid chars
- .replace(/\.{2,}/g, ".") // collapse multiple dots
- .replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
- .replace(/^\.+|\.+$/g, ""); // strip leading/trailing dots
+ .normalize("NFC")
+ .replace(/[^\p{L}\p{N}.-]/gu, "") // allow Unicode
+ .replace(/\.{2,}/g, ".") // collapse multiple dots
+ .replace(/^-+|-+$/g, "") // strip leading/trailing hyphens
+ .replace(/^\.+|\.+$/g, "") // strip leading/trailing dots
+ .replace(/(\.-)|(-\.)/g, "."); // fix illegal dot-hyphen combos
}
-
-
export function validateByDomainType(subdomain: string, domainType: { type: "provided-search" | "organization"; domainType?: "ns" | "cname" | "wildcard" } ): boolean {
if (!domainType) return false;
@@ -47,7 +50,7 @@ export function validateByDomainType(subdomain: string, domainType: { type: "pro
export const isValidSubdomainStructure = (input: string): boolean => {
- const regex = /^(?!-)([a-zA-Z0-9-]{1,63})(? {
+