mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Show/allow unicode domain name
This commit is contained in:
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -1494,5 +1494,7 @@
|
||||
"documentation": "documentation"
|
||||
},
|
||||
"convertButton": "Convert This Node to Managed Self-Hosted"
|
||||
}
|
||||
},
|
||||
"internationaldomaindetected": "International Domain Detected",
|
||||
"willbestoredas": "Will be stored as:"
|
||||
}
|
||||
|
||||
@@ -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: "
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -1457,5 +1457,7 @@
|
||||
"autoLoginRedirecting": "로그인으로 리디렉션 중...",
|
||||
"autoLoginError": "자동 로그인 오류",
|
||||
"autoLoginErrorNoRedirectUrl": "ID 공급자로부터 리디렉션 URL을 받지 못했습니다.",
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패."
|
||||
"autoLoginErrorGeneratingUrl": "인증 URL 생성 실패.",
|
||||
"internationaldomaindetected": "국제 도메인 감지됨",
|
||||
"willbestoredas": "다음과 같이 저장됩니다."
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -1457,5 +1457,7 @@
|
||||
"autoLoginRedirecting": "Перенаправление к входу...",
|
||||
"autoLoginError": "Ошибка автоматического входа",
|
||||
"autoLoginErrorNoRedirectUrl": "URL-адрес перенаправления не получен от провайдера удостоверения.",
|
||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации."
|
||||
"autoLoginErrorGeneratingUrl": "Не удалось сгенерировать URL-адрес аутентификации.",
|
||||
"internationaldomaindetected": "Обнаружен международный домен",
|
||||
"willbestoredas": "Будет сохранен как:"
|
||||
}
|
||||
|
||||
@@ -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:"
|
||||
}
|
||||
|
||||
@@ -1457,5 +1457,7 @@
|
||||
"autoLoginRedirecting": "重定向到登录...",
|
||||
"autoLoginError": "自动登录错误",
|
||||
"autoLoginErrorNoRedirectUrl": "未从身份提供商收到重定向URL。",
|
||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。"
|
||||
"autoLoginErrorGeneratingUrl": "生成身份验证URL失败。",
|
||||
"internationaldomaindetected": "检测到国际域名",
|
||||
"willbestoredas": "将存储为:"
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
<FormLabel>{t("domain")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="example.com"
|
||||
placeholder="example.com, café.com, 日本.com"
|
||||
{...field}
|
||||
/>
|
||||
</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 />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -206,66 +275,73 @@ export default function CreateDomainForm({
|
||||
|
||||
<div className="space-y-4">
|
||||
{createdDomain.nsRecords &&
|
||||
createdDomain.nsRecords.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainNsRecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("createDomainRecord")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainType"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
NS
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{baseDomain}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainValue"
|
||||
)}
|
||||
</span>
|
||||
{createdDomain.nsRecords.map(
|
||||
(
|
||||
nsRecord,
|
||||
index
|
||||
) => (
|
||||
<div
|
||||
className="flex justify-between items-center"
|
||||
key={index}
|
||||
>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
nsRecord
|
||||
}
|
||||
/>
|
||||
createdDomain.nsRecords.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainNsRecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("createDomainRecord")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainType"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
NS
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-mono block">
|
||||
{fromPunycode(baseDomain)}
|
||||
</span>
|
||||
{fromPunycode(baseDomain) !== baseDomain && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
({baseDomain})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainValue"
|
||||
)}
|
||||
</span>
|
||||
{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.length > 0 && (
|
||||
@@ -307,11 +383,16 @@ export default function CreateDomainForm({
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
cnameRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-mono block">
|
||||
{fromPunycode(cnameRecord.baseDomain)}
|
||||
</span>
|
||||
{fromPunycode(cnameRecord.baseDomain) !== cnameRecord.baseDomain && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
({cnameRecord.baseDomain})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
@@ -374,11 +455,16 @@ export default function CreateDomainForm({
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
aRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-mono block">
|
||||
{fromPunycode(aRecord.baseDomain)}
|
||||
</span>
|
||||
{fromPunycode(aRecord.baseDomain) !== aRecord.baseDomain && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
({aRecord.baseDomain})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
@@ -390,7 +476,7 @@ export default function CreateDomainForm({
|
||||
{
|
||||
aRecord.value
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
@@ -440,11 +526,16 @@ export default function CreateDomainForm({
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
txtRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
<div className="text-right">
|
||||
<span className="text-sm font-mono block">
|
||||
{fromPunycode(txtRecord.baseDomain)}
|
||||
</span>
|
||||
{fromPunycode(txtRecord.baseDomain) !== txtRecord.baseDomain && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
({txtRecord.baseDomain})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
@@ -513,4 +604,4 @@ export default function CreateDomainForm({
|
||||
</CredenzaContent>
|
||||
</Credenza>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<ListDomainsResponse>
|
||||
>(`/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);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@@ -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 (
|
||||
<Alert>
|
||||
@@ -34,9 +38,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{authInfo.password ||
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ? (
|
||||
authInfo.pincode ||
|
||||
authInfo.sso ||
|
||||
authInfo.whitelist ? (
|
||||
<div className="flex items-start space-x-2 text-green-500">
|
||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||
<span>{t("protected")}</span>
|
||||
|
||||
@@ -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 { toUnicode } from "punycode";
|
||||
|
||||
export default function GeneralForm() {
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -319,10 +325,10 @@ export default function GeneralForm() {
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 { toUnicode } from 'punycode';
|
||||
import { DomainRow } from "../../domains/DomainsTable";
|
||||
|
||||
const baseResourceFormSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || "")}/`
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -406,6 +408,12 @@ export default function DomainPicker2({
|
||||
const hasMoreProvided =
|
||||
sortedAvailableOptions.length > providedDomainsShown;
|
||||
|
||||
|
||||
const isValidDomainCharacter = (char: string) => {
|
||||
// Allow Unicode letters, numbers, hyphens, and periods
|
||||
return /[\p{L}\p{N}.-]/u.test(char);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -424,8 +432,8 @@ export default function DomainPicker2({
|
||||
showProvidedDomainSearch
|
||||
? ""
|
||||
: showSubdomainInput
|
||||
? ""
|
||||
: t("domainPickerNotAvailableForCname")
|
||||
? ""
|
||||
: t("domainPickerNotAvailableForCname")
|
||||
}
|
||||
disabled={
|
||||
!showSubdomainInput && !showProvidedDomainSearch
|
||||
@@ -436,10 +444,16 @@ export default function DomainPicker2({
|
||||
"border-red-500 focus:border-red-500"
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const rawInput = e.target.value;
|
||||
const validInput = rawInput
|
||||
.split("")
|
||||
.filter((char) => isValidDomainCharacter(char))
|
||||
.join("");
|
||||
|
||||
if (showProvidedDomainSearch) {
|
||||
handleProvidedDomainInputChange(e.target.value);
|
||||
handleProvidedDomainInputChange(validInput);
|
||||
} 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.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{showSubdomainInput && !subdomainInput && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("domainPickerEnterSubdomainOrLeaveBlank")}
|
||||
@@ -474,7 +487,7 @@ export default function DomainPicker2({
|
||||
{selectedBaseDomain ? (
|
||||
<div className="flex items-center space-x-2 min-w-0 flex-1">
|
||||
{selectedBaseDomain.type ===
|
||||
"organization" ? null : (
|
||||
"organization" ? null : (
|
||||
<Zap className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
<span className="truncate">
|
||||
@@ -568,67 +581,67 @@ export default function DomainPicker2({
|
||||
</CommandGroup>
|
||||
{(build === "saas" ||
|
||||
build === "enterprise") && (
|
||||
<CommandSeparator className="my-2" />
|
||||
)}
|
||||
<CommandSeparator className="my-2" />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{(build === "saas" ||
|
||||
build === "enterprise") && (
|
||||
<CommandGroup
|
||||
heading={
|
||||
build === "enterprise"
|
||||
? t(
|
||||
"domainPickerProvidedDomains"
|
||||
)
|
||||
: t("domainPickerFreeDomains")
|
||||
}
|
||||
className="py-2"
|
||||
>
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
key="provided-search"
|
||||
onSelect={() =>
|
||||
handleBaseDomainSelect({
|
||||
id: "provided-search",
|
||||
domain:
|
||||
build ===
|
||||
"enterprise"
|
||||
<CommandGroup
|
||||
heading={
|
||||
build === "enterprise"
|
||||
? t(
|
||||
"domainPickerProvidedDomains"
|
||||
)
|
||||
: t("domainPickerFreeDomains")
|
||||
}
|
||||
className="py-2"
|
||||
>
|
||||
<CommandList>
|
||||
<CommandItem
|
||||
key="provided-search"
|
||||
onSelect={() =>
|
||||
handleBaseDomainSelect({
|
||||
id: "provided-search",
|
||||
domain:
|
||||
build ===
|
||||
"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"
|
||||
: "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"
|
||||
: "Free Provided Domain"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"domainPickerSearchForAvailableDomains"
|
||||
: "Free Provided Domain"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"domainPickerSearchForAvailableDomains"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary",
|
||||
selectedBaseDomain?.id ===
|
||||
"provided-search"
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4 text-primary",
|
||||
selectedBaseDomain?.id ===
|
||||
"provided-search"
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
)}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@@ -684,7 +697,7 @@ export default function DomainPicker2({
|
||||
htmlFor={option.domainNamespaceId}
|
||||
data-state={
|
||||
selectedProvidedDomain?.domainNamespaceId ===
|
||||
option.domainNamespaceId
|
||||
option.domainNamespaceId
|
||||
? "checked"
|
||||
: "unchecked"
|
||||
}
|
||||
@@ -764,4 +777,4 @@ function debounce<T extends (...args: any[]) => any>(
|
||||
func(...args);
|
||||
}, wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user