mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-29 06:10:47 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e986def78 | ||
|
|
d16a05959d | ||
|
|
7e58e0b490 | ||
|
|
9b01aecf3c | ||
|
|
86043fd5f8 | ||
|
|
372a1758e9 | ||
|
|
0a2b1d9e53 | ||
|
|
e562946308 | ||
|
|
398e15b3c6 | ||
|
|
c225a54dbe | ||
|
|
5148988dcc | ||
|
|
28b57ba652 | ||
|
|
9c7e74ef37 | ||
|
|
330b28ad9c | ||
|
|
da7166a7ea | ||
|
|
e8793c5d8d |
@@ -16,7 +16,7 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
||||
|
||||
<div align="center">
|
||||
<h5>
|
||||
<a href="https://fossorial.io">
|
||||
<a href="https://digpangolin.com">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
@@ -111,7 +111,7 @@ Host the full application on your own server or on the cloud with a VPS. Take a
|
||||
|
||||
### Pangolin Cloud
|
||||
|
||||
Easy to use with simple pay as you go pricing. [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||
Easy to use with simple [pay as you go pricing](https://digpangolin.io/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
||||
|
||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ orgs:
|
||||
email:
|
||||
smtp_host: "{{.EmailSMTPHost}}"
|
||||
smtp_port: {{.EmailSMTPPort}}
|
||||
smtp_user: "{{.EmailSMTPUser}allow_base_domain_resources}"
|
||||
smtp_user: "{{.EmailSMTPUser}}"
|
||||
smtp_pass: "{{.EmailSMTPPass}}"
|
||||
no_reply: "{{.EmailNoReply}}"
|
||||
{{end}}
|
||||
|
||||
@@ -1266,6 +1266,7 @@
|
||||
"createDomainName": "Name:",
|
||||
"createDomainValue": "Value:",
|
||||
"createDomainCnameRecords": "CNAME Records",
|
||||
"createDomainARecords": "A Records",
|
||||
"createDomainRecordNumber": "Record {number}",
|
||||
"createDomainTxtRecords": "TXT Records",
|
||||
"createDomainSaveTheseRecords": "Save These Records",
|
||||
|
||||
@@ -128,7 +128,9 @@ export const configSchema = z
|
||||
.object({
|
||||
http_entrypoint: z.string().optional().default("web"),
|
||||
https_entrypoint: z.string().optional().default("websecure"),
|
||||
additional_middlewares: z.array(z.string()).optional()
|
||||
additional_middlewares: z.array(z.string()).optional(),
|
||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
@@ -157,13 +159,16 @@ export const configSchema = z
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
orgs: z.object({
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
subnet_group: z.string().optional().default("100.90.128.0/24")
|
||||
}).optional().default({
|
||||
block_size: 24,
|
||||
subnet_group: "100.90.128.0/24"
|
||||
}),
|
||||
orgs: z
|
||||
.object({
|
||||
block_size: z.number().positive().gt(0).optional().default(24),
|
||||
subnet_group: z.string().optional().default("100.90.128.0/24")
|
||||
})
|
||||
.optional()
|
||||
.default({
|
||||
block_size: 24,
|
||||
subnet_group: "100.90.128.0/24"
|
||||
}),
|
||||
rate_limits: z
|
||||
.object({
|
||||
global: z
|
||||
@@ -242,7 +247,7 @@ export const configSchema = z
|
||||
{
|
||||
message: "At least one domain must be defined"
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function readConfigFile() {
|
||||
const loadConfig = (configPath: string) => {
|
||||
|
||||
@@ -29,6 +29,7 @@ export type CreateDomainResponse = {
|
||||
domainId: string;
|
||||
nsRecords?: string[];
|
||||
cnameRecords?: { baseDomain: string; value: string }[];
|
||||
aRecords?: { baseDomain: string; value: string }[];
|
||||
txtRecords?: { baseDomain: string; value: string }[];
|
||||
};
|
||||
|
||||
@@ -97,6 +98,7 @@ export async function createOrgDomain(
|
||||
}
|
||||
|
||||
let numOrgDomains: OrgDomains[] | undefined;
|
||||
let aRecords: CreateDomainResponse["aRecords"];
|
||||
let cnameRecords: CreateDomainResponse["cnameRecords"];
|
||||
let txtRecords: CreateDomainResponse["txtRecords"];
|
||||
let nsRecords: CreateDomainResponse["nsRecords"];
|
||||
@@ -239,7 +241,7 @@ export async function createOrgDomain(
|
||||
}
|
||||
];
|
||||
} else if (type === "wildcard") {
|
||||
cnameRecords = [
|
||||
aRecords = [
|
||||
{
|
||||
value: `Server IP Address`,
|
||||
baseDomain: `*.${baseDomain}`
|
||||
@@ -271,7 +273,8 @@ export async function createOrgDomain(
|
||||
domainId: returned.domainId,
|
||||
cnameRecords,
|
||||
txtRecords,
|
||||
nsRecords
|
||||
nsRecords,
|
||||
aRecords
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -258,101 +258,13 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects the most suitable exit node from a list of ping results.
|
||||
*
|
||||
* The selection algorithm follows these steps:
|
||||
*
|
||||
* 1. **Filter Invalid Nodes**: Excludes nodes with errors or zero weight.
|
||||
*
|
||||
* 2. **Sort by Latency**: Sorts valid nodes in ascending order of latency.
|
||||
*
|
||||
* 3. **Preferred Selection**:
|
||||
* - If the lowest-latency node has sufficient capacity (≥10% weight),
|
||||
* check if a previously connected node is also acceptable.
|
||||
* - The previously connected node is preferred if its latency is within
|
||||
* 30ms or 15% of the best node’s latency.
|
||||
*
|
||||
* 4. **Fallback to Next Best**:
|
||||
* - If the lowest-latency node is under capacity, find the next node
|
||||
* with acceptable capacity.
|
||||
*
|
||||
* 5. **Final Fallback**:
|
||||
* - If no nodes meet the capacity threshold, fall back to the node
|
||||
* with the highest weight (i.e., most available capacity).
|
||||
*
|
||||
*/
|
||||
function selectBestExitNode(
|
||||
pingResults: ExitNodePingResult[]
|
||||
): ExitNodePingResult | null {
|
||||
const MIN_CAPACITY_THRESHOLD = 0.1;
|
||||
const LATENCY_TOLERANCE_MS = 30;
|
||||
const LATENCY_TOLERANCE_PERCENT = 0.15;
|
||||
|
||||
// Filter out invalid nodes
|
||||
const validNodes = pingResults.filter((n) => !n.error && n.weight > 0);
|
||||
|
||||
if (validNodes.length === 0) {
|
||||
logger.error("No valid exit nodes available");
|
||||
if (!pingResults || pingResults.length === 0) {
|
||||
logger.warn("No ping results provided");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sort by latency (ascending)
|
||||
const sortedNodes = validNodes
|
||||
.slice()
|
||||
.sort((a, b) => a.latencyMs - b.latencyMs);
|
||||
const lowestLatencyNode = sortedNodes[0];
|
||||
|
||||
logger.info(
|
||||
`Lowest latency node: ${lowestLatencyNode.exitNodeName} (${lowestLatencyNode.latencyMs} ms, weight=${lowestLatencyNode.weight.toFixed(2)})`
|
||||
);
|
||||
|
||||
// If lowest latency node has enough capacity, check if previously connected node is acceptable
|
||||
if (lowestLatencyNode.weight >= MIN_CAPACITY_THRESHOLD) {
|
||||
const previouslyConnectedNode = sortedNodes.find(
|
||||
(n) =>
|
||||
n.wasPreviouslyConnected && n.weight >= MIN_CAPACITY_THRESHOLD
|
||||
);
|
||||
|
||||
if (previouslyConnectedNode) {
|
||||
const latencyDiff =
|
||||
previouslyConnectedNode.latencyMs - lowestLatencyNode.latencyMs;
|
||||
const percentDiff = latencyDiff / lowestLatencyNode.latencyMs;
|
||||
|
||||
if (
|
||||
latencyDiff <= LATENCY_TOLERANCE_MS ||
|
||||
percentDiff <= LATENCY_TOLERANCE_PERCENT
|
||||
) {
|
||||
logger.info(
|
||||
`Sticking with previously connected node: ${previouslyConnectedNode.exitNodeName} ` +
|
||||
`(${previouslyConnectedNode.latencyMs} ms), latency diff = ${latencyDiff.toFixed(1)}ms ` +
|
||||
`/ ${(percentDiff * 100).toFixed(1)}%.`
|
||||
);
|
||||
return previouslyConnectedNode;
|
||||
}
|
||||
}
|
||||
|
||||
return lowestLatencyNode;
|
||||
}
|
||||
|
||||
// Otherwise, find the next node (after the lowest) that has enough capacity
|
||||
for (let i = 1; i < sortedNodes.length; i++) {
|
||||
const node = sortedNodes[i];
|
||||
if (node.weight >= MIN_CAPACITY_THRESHOLD) {
|
||||
logger.info(
|
||||
`Lowest latency node under capacity. Using next best: ${node.exitNodeName} ` +
|
||||
`(${node.latencyMs} ms, weight=${node.weight.toFixed(2)})`
|
||||
);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: pick the highest weight node
|
||||
const fallbackNode = validNodes.reduce((a, b) =>
|
||||
a.weight > b.weight ? a : b
|
||||
);
|
||||
logger.warn(
|
||||
`No nodes with ≥10% weight. Falling back to highest capacity node: ${fallbackNode.exitNodeName}`
|
||||
);
|
||||
return fallbackNode;
|
||||
return pingResults[0];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from "express";
|
||||
import { db, exitNodes } from "@server/db";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { and, eq, inArray, or, isNull } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import config from "@server/lib/config";
|
||||
@@ -27,13 +27,9 @@ export async function traefikConfigProvider(
|
||||
})
|
||||
.from(exitNodes)
|
||||
.where(eq(exitNodes.name, exitNodeName));
|
||||
if (!exitNode) {
|
||||
logger.error(
|
||||
`Exit node with name ${exitNodeName} not found in the database`
|
||||
);
|
||||
return [];
|
||||
if (exitNode) {
|
||||
currentExitNodeId = exitNode.exitNodeId;
|
||||
}
|
||||
currentExitNodeId = exitNode.exitNodeId;
|
||||
} else {
|
||||
const [exitNode] = await tx
|
||||
.select({
|
||||
@@ -42,12 +38,9 @@ export async function traefikConfigProvider(
|
||||
.from(exitNodes)
|
||||
.limit(1);
|
||||
|
||||
if (!exitNode) {
|
||||
logger.error("No exit node found in the database");
|
||||
return [];
|
||||
if (exitNode) {
|
||||
currentExitNodeId = exitNode.exitNodeId;
|
||||
}
|
||||
|
||||
currentExitNodeId = exitNode.exitNodeId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +61,7 @@ export async function traefikConfigProvider(
|
||||
siteId: sites.siteId,
|
||||
type: sites.type,
|
||||
subnet: sites.subnet,
|
||||
exitNodeId: sites.exitNodeId,
|
||||
exitNodeId: sites.exitNodeId
|
||||
},
|
||||
enabled: resources.enabled,
|
||||
stickySession: resources.stickySession,
|
||||
@@ -77,7 +70,12 @@ export async function traefikConfigProvider(
|
||||
})
|
||||
.from(resources)
|
||||
.innerJoin(sites, eq(sites.siteId, resources.siteId))
|
||||
.where(eq(sites.exitNodeId, currentExitNodeId));
|
||||
.where(
|
||||
or(
|
||||
eq(sites.exitNodeId, currentExitNodeId),
|
||||
isNull(sites.exitNodeId)
|
||||
)
|
||||
);
|
||||
|
||||
// Get all resource IDs from the first query
|
||||
const resourceIds = resourcesWithRelations.map((r) => r.resourceId);
|
||||
@@ -216,22 +214,29 @@ export async function traefikConfigProvider(
|
||||
|
||||
const configDomain = config.getDomain(resource.domainId);
|
||||
|
||||
let tls = {};
|
||||
if (configDomain) {
|
||||
tls = {
|
||||
certResolver: configDomain.cert_resolver,
|
||||
...(configDomain.prefer_wildcard_cert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
let certResolver: string, preferWildcardCert: boolean;
|
||||
if (!configDomain) {
|
||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||
preferWildcardCert =
|
||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||
} else {
|
||||
certResolver = configDomain.cert_resolver;
|
||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||
}
|
||||
|
||||
const tls = {
|
||||
certResolver: certResolver,
|
||||
...(preferWildcardCert
|
||||
? {
|
||||
domains: [
|
||||
{
|
||||
main: wildCard
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
const additionalMiddlewares =
|
||||
config.getRawConfig().traefik.additional_middlewares || [];
|
||||
|
||||
@@ -284,7 +289,8 @@ export async function traefikConfigProvider(
|
||||
} else if (site.type === "newt") {
|
||||
if (
|
||||
!target.internalPort ||
|
||||
!target.method || !site.subnet
|
||||
!target.method ||
|
||||
!site.subnet
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -205,188 +205,267 @@ export default function CreateDomainForm({
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-4">
|
||||
{domainType === "ns" &&
|
||||
createdDomain.nsRecords && (
|
||||
{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
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdDomain.cnameRecords &&
|
||||
createdDomain.cnameRecords.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainNsRecords")}
|
||||
{t("createDomainCnameRecords")}
|
||||
</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
|
||||
}
|
||||
>
|
||||
{createdDomain.cnameRecords.map(
|
||||
(cnameRecord, index) => (
|
||||
<InfoSection
|
||||
key={index}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
{t(
|
||||
"createDomainRecordNumber",
|
||||
{
|
||||
number:
|
||||
index +
|
||||
1
|
||||
}
|
||||
)}
|
||||
</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">
|
||||
CNAME
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
cnameRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainValue"
|
||||
)}
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
nsRecord
|
||||
cnameRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{domainType === "cname" ||
|
||||
(domainType == "wildcard" && (
|
||||
<>
|
||||
{createdDomain.cnameRecords &&
|
||||
createdDomain.cnameRecords
|
||||
.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainCnameRecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.cnameRecords.map(
|
||||
(
|
||||
cnameRecord,
|
||||
index
|
||||
) => (
|
||||
<InfoSection
|
||||
key={
|
||||
index
|
||||
}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
{t("createDomainRecordNumber", { number: index + 1 })}
|
||||
</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">
|
||||
CNAME
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t("createDomainName")}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
cnameRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t("createDomainValue")}
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
cnameRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</InfoSections>
|
||||
</div>
|
||||
{createdDomain.aRecords &&
|
||||
createdDomain.aRecords.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainARecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.aRecords.map(
|
||||
(aRecord, index) => (
|
||||
<InfoSection
|
||||
key={index}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
{t(
|
||||
"createDomainRecordNumber",
|
||||
{
|
||||
number:
|
||||
index +
|
||||
1
|
||||
}
|
||||
)}
|
||||
</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">
|
||||
A
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
aRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainValue"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
aRecord.value
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
|
||||
{createdDomain.txtRecords &&
|
||||
createdDomain.txtRecords
|
||||
.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainTxtRecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.txtRecords.map(
|
||||
(
|
||||
txtRecord,
|
||||
index
|
||||
) => (
|
||||
<InfoSection
|
||||
key={
|
||||
index
|
||||
}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
{t("createDomainRecordNumber", { number: index + 1 })}
|
||||
</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">
|
||||
TXT
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t("createDomainName")}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
txtRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t("createDomainValue")}
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
txtRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</InfoSections>
|
||||
</div>
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
{createdDomain.txtRecords &&
|
||||
createdDomain.txtRecords.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium mb-3">
|
||||
{t("createDomainTxtRecords")}
|
||||
</h3>
|
||||
<InfoSections cols={1}>
|
||||
{createdDomain.txtRecords.map(
|
||||
(txtRecord, index) => (
|
||||
<InfoSection
|
||||
key={index}
|
||||
>
|
||||
<InfoSectionTitle>
|
||||
{t(
|
||||
"createDomainRecordNumber",
|
||||
{
|
||||
number:
|
||||
index +
|
||||
1
|
||||
}
|
||||
)}
|
||||
</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">
|
||||
TXT
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainName"
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-mono">
|
||||
{
|
||||
txtRecord.baseDomain
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium">
|
||||
{t(
|
||||
"createDomainValue"
|
||||
)}
|
||||
</span>
|
||||
<CopyToClipboard
|
||||
text={
|
||||
txtRecord.value
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</InfoSections>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{build == "saas" ||
|
||||
@@ -397,7 +476,9 @@ export default function CreateDomainForm({
|
||||
{t("createDomainSaveTheseRecords")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("createDomainSaveTheseRecordsDescription")}
|
||||
{t(
|
||||
"createDomainSaveTheseRecordsDescription"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user