From 5601da8225b14b8db0bfd3d69ef4e2983e089f41 Mon Sep 17 00:00:00 2001 From: Arman Date: Tue, 1 Apr 2025 17:21:49 +0200 Subject: [PATCH 01/79] feat: import txt? --- src/lib/components/paginationInline.svelte | 9 +- src/lib/helpers/domains.test.ts | 379 ++++++++++++++++++ src/lib/helpers/domains.ts | 256 ++++++++++++ .../domain-[domain]/importRecordModal.svelte | 119 +----- 4 files changed, 638 insertions(+), 125 deletions(-) create mode 100644 src/lib/helpers/domains.test.ts create mode 100644 src/lib/helpers/domains.ts diff --git a/src/lib/components/paginationInline.svelte b/src/lib/components/paginationInline.svelte index 3e718cf0a..924a3f6ef 100644 --- a/src/lib/components/paginationInline.svelte +++ b/src/lib/components/paginationInline.svelte @@ -54,14 +54,7 @@ {#if !hidePages} - + {:else} { + it('should parse standard zone file with origin', () => { + const content = `$ORIGIN example.com. ; designates the start of this zone file in the namespace +$TTL 3600 ; default expiration time (in seconds) of all RRs without their own TTL value +example.com. IN SOA ns.example.com. username.example.com. ( 2020091025 7200 3600 1209600 3600 ) +example.com. IN NS ns ; ns.example.com is a nameserver for example.com +example.com. IN NS ns.somewhere.example. ; ns.somewhere.example is a backup nameserver for example.com +example.com. IN MX 10 mail.example.com. ; mail.example.com is the mailserver for example.com +@ IN MX 20 mail2.example.com. ; equivalent to above line, "@" represents zone origin +@ IN MX 50 mail3 ; equivalent to above line, but using a relative host name +example.com. IN A 192.0.2.1 ; IPv4 address for example.com + IN AAAA 2001:db8:10::1 ; IPv6 address for example.com +ns IN A 192.0.2.2 ; IPv4 address for ns.example.com + IN AAAA 2001:db8:10::2 ; IPv6 address for ns.example.com +www IN CNAME example.com. ; www.example.com is an alias for example.com +wwwtest IN CNAME www ; wwwtest.example.com is another alias for www.example.com +mail IN A 192.0.2.3 ; IPv4 address for mail.example.com +mail2 IN A 192.0.2.4 ; IPv4 address for mail2.example.com +mail3 IN A 192.0.2.5 ; IPv4 address for mail3.example.com`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(2); + expect(result.NS[0].name).toBe('example.com'); + expect(result.NS[0].value).toBe('ns'); + expect(result.NS[1].name).toBe('example.com'); + expect(result.NS[1].value).toBe('ns.somewhere.example'); + + expect(result.MX).toHaveLength(3); + expect(result.MX[0].name).toBe('example.com'); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('mail.example.com'); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].priority).toBe(20); + expect(result.MX[1].value).toBe('mail2.example.com'); + expect(result.MX[2].name).toBe(''); + expect(result.MX[2].priority).toBe(50); + expect(result.MX[2].value).toBe('mail3'); + + expect(result.A).toHaveLength(5); + expect(result.A[0].name).toBe('example.com'); + expect(result.A[0].value).toBe('192.0.2.1'); + expect(result.A[1].name).toBe('ns'); + expect(result.A[1].value).toBe('192.0.2.2'); + expect(result.A[2].name).toBe('mail'); + expect(result.A[2].value).toBe('192.0.2.3'); + expect(result.A[3].name).toBe('mail2'); + expect(result.A[3].value).toBe('192.0.2.4'); + expect(result.A[4].name).toBe('mail3'); + expect(result.A[4].value).toBe('192.0.2.5'); + + expect(result.AAAA).toHaveLength(2); + expect(result.AAAA[0].name).toBe('example.com'); + expect(result.AAAA[0].value).toBe('2001:db8:10::1'); + expect(result.AAAA[1].name).toBe('ns'); + expect(result.AAAA[1].value).toBe('2001:db8:10::2'); + + expect(result.CNAME).toHaveLength(2); + expect(result.CNAME[0].name).toBe('www'); + expect(result.CNAME[0].value).toBe('example.com'); + expect(result.CNAME[1].name).toBe('wwwtest'); + expect(result.CNAME[1].value).toBe('www'); + }); + + it('should parse localhost zone file', () => { + const content = `$ORIGIN localhost. +@ 86400 IN SOA @ root ( + 1999010100 ; serial + 10800 ; refresh (3 hours) + 900 ; retry (15 minutes) + 604800 ; expire (1 week) + 86400 ; minimum (1 day) + ) +@ 86400 IN NS @ +@ 86400 IN A 127.0.0.1 +@ 86400 IN AAAA ::1`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(1); + expect(result.NS[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.NS[0].value).toBe('@'); + expect(result.NS[0].ttl).toBe(86400); + + expect(result.A).toHaveLength(1); + expect(result.A[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.A[0].value).toBe('127.0.0.1'); + expect(result.A[0].ttl).toBe(86400); + + expect(result.AAAA).toHaveLength(1); + expect(result.AAAA[0].name).toBe(''); // @ symbol is converted to empty string + expect(result.AAAA[0].value).toBe('::1'); + expect(result.AAAA[0].ttl).toBe(86400); + }); + + it('should parse complex zone file with multiple record types and comments', () => { + const content = `; Exported (y-m-d hh:mm:ss): 2019-01-10 13:05:04 +; +; This file is intended for use for informational and archival +; purposes ONLY and MUST be edited before use on a production +; DNS server. +; +; In particular, you must update the SOA record with the correct +; authoritative name server and contact e-mail address information, +; and add the correct NS records for the name servers which will +; be authoritative for this domain. +; +; For further information, please consult the BIND documentation +; located on the following website: +; +; http://www.isc.org/ +; +; And RFC 1035: +; +; http://www.ietf.org/rfc/rfc1035.txt +; +; Please note that we do NOT offer technical support for any use +; of this zone data, the BIND name server, or any other third- +; party DNS software. +; +; Use at your own risk. +; SOA Record +example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. ( + 2018122702 + 28800 + 7200 + 604800 + 3600 + ) +; A Records +@ 600 IN A 192.0.2.249 +blog 10800 IN A 192.0.2.255 +dev 1800 IN A 192.0.2.254 +dev01 1800 IN A 192.0.2.253 +dev02 1800 IN A 192.0.2.252 +dev03 1800 IN A 192.0.2.251 +dev04 1800 IN A 192.0.2.250 +; CNAME Records +abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com. +akamai 600 IN CNAME www.example.com.edgekey.net. +email 3600 IN CNAME email.secureserver.net. +; MX Records +@ 604800 IN MX 10 amlxe.l.google.com. +@ 604800 IN MX 10 aplxe.l.google.com. +; TXT Records +@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg" +@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918"`; + + const result = parseDnsRecords(content); + + expect(result.A).toHaveLength(7); + expect(result.A[0].name).toBe(''); + expect(result.A[0].value).toBe('192.0.2.249'); + expect(result.A[0].ttl).toBe(600); + expect(result.A[1].name).toBe('blog'); + expect(result.A[1].value).toBe('192.0.2.255'); + expect(result.A[1].ttl).toBe(10800); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71'); + expect(result.CNAME[0].value).toBe('verify.bing.com'); + expect(result.CNAME[0].ttl).toBe(1800); + expect(result.CNAME[1].name).toBe('akamai'); + expect(result.CNAME[1].value).toBe('www.example.com.edgekey.net'); + expect(result.CNAME[1].ttl).toBe(600); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].value).toBe('amlxe.l.google.com'); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].ttl).toBe(604800); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].value).toBe('aplxe.l.google.com'); + expect(result.MX[1].priority).toBe(10); + expect(result.MX[1].ttl).toBe(604800); + + expect(result.TXT).toHaveLength(2); + expect(result.TXT[0].name).toBe(''); + expect(result.TXT[0].value).toBe( + 'google-site-verification=3J82-80dbMyCo5Q5C1G11JszeOnZPGCSYlHcPcXg' + ); + expect(result.TXT[0].ttl).toBe(3600); + expect(result.TXT[1].name).toBe(''); + expect(result.TXT[1].value).toBe( + 'google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB743xsbX918' + ); + expect(result.TXT[1].ttl).toBe(3600); + }); + + it('should parse zone file with origin prefix', () => { + const content = `$ORIGIN example.com. +example.com. 3600 IN SOA ns41.domaincontrol.com. dns.net. ( + 2018122702 + 28800 + 7200 + 604800 + 3600 + ) + +; A Records +@ 600 IN A 192.0.2.249 +blog 10800 IN A 192.0.2.255 +dev 1800 IN A 192.0.2.254 +dev01 1800 IN A 192.0.2.253 +dev02 1800 IN A 192.0.2.252 +dev03 1800 IN A 192.0.2.251 +dev04 1800 IN A 192.0.2.250 +abc123b432dc7785b7ef31f04f25c3e71 1800 IN CNAME verify.bing.com. +; CNAME Records +akamai 600 IN CNAME www.example.edgekey.net. +email 3600 IN CNAME email.secureserver.net. +; MX Records +@ 604800 IN MX 10 amlxe.l.google.com. +@ 604800 IN MX 10 aplxe.l.google.com. +; TXT Records +@ 3600 IN TXT "google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg" +@ 3600 IN TXT "google-site-verification=eS_QPYLE_W4nduSrlN-cddxG7ZqOnB7k7uIG7qrsyu8"`; + + const result = parseDnsRecords(content); + + expect(result.A).toHaveLength(7); + expect(result.A[0].name).toBe(''); + expect(result.A[0].value).toBe('192.0.2.249'); + expect(result.A[0].ttl).toBe(600); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('abc123b432dc7785b7ef31f04f25c3e71'); + expect(result.CNAME[0].value).toBe('verify.bing.com'); + expect(result.CNAME[1].name).toBe('akamai'); + expect(result.CNAME[1].value).toBe('www.example.edgekey.net'); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('amlxe.l.google.com'); + + expect(result.TXT).toHaveLength(2); + expect(result.TXT[0].name).toBe(''); + expect(result.TXT[0].value).toBe( + 'google-site-verification=3J82-80dbMyCo5Q5C1GM8os1VYVEOnZPGCSYlHcPcXg' + ); + }); + + it('should parse complex zone file with multiple servers', () => { + const content = `$ORIGIN example.com. +$TTL 86400 +@ IN SOA dns1.example.com. hostmaster.example.com. ( + 2001062501 ; serial + 21600 ; refresh after 6 hours + 3600 ; retry after 1 hour + 604800 ; expire after 1 week + 86400 ) ; minimum TTL of 1 day + + + IN NS dns1.example.com. + IN NS dns2.example.com. + + + IN MX 10 mail.example.com. + IN MX 20 mail2.example.com. + + +dns1 IN A 10.0.1.1 +dns2 IN A 10.0.1.2 + + +server1 IN A 10.0.1.5 +server2 IN A 10.0.1.6 + + +ftp IN A 10.0.1.3 + IN A 10.0.1.4 + +mail IN CNAME server1 +mail2 IN CNAME server2 + + +www IN CNAME server1`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(2); + expect(result.NS[0].name).toBe(''); + expect(result.NS[0].value).toBe('dns1.example.com'); + expect(result.NS[1].name).toBe(''); + expect(result.NS[1].value).toBe('dns2.example.com'); + + expect(result.MX).toHaveLength(2); + expect(result.MX[0].name).toBe(''); + expect(result.MX[0].priority).toBe(10); + expect(result.MX[0].value).toBe('mail.example.com'); + expect(result.MX[1].name).toBe(''); + expect(result.MX[1].priority).toBe(20); + expect(result.MX[1].value).toBe('mail2.example.com'); + + expect(result.A).toHaveLength(6); + expect(result.A[0].name).toBe('dns1'); + expect(result.A[0].value).toBe('10.0.1.1'); + expect(result.A[1].name).toBe('dns2'); + expect(result.A[1].value).toBe('10.0.1.2'); + expect(result.A[2].name).toBe('server1'); + expect(result.A[2].value).toBe('10.0.1.5'); + expect(result.A[3].name).toBe('server2'); + expect(result.A[3].value).toBe('10.0.1.6'); + expect(result.A[4].name).toBe('ftp'); + expect(result.A[4].value).toBe('10.0.1.3'); + expect(result.A[5].name).toBe('ftp'); + expect(result.A[5].value).toBe('10.0.1.4'); + + expect(result.CNAME).toHaveLength(3); + expect(result.CNAME[0].name).toBe('mail'); + expect(result.CNAME[0].value).toBe('server1'); + expect(result.CNAME[1].name).toBe('mail2'); + expect(result.CNAME[1].value).toBe('server2'); + expect(result.CNAME[2].name).toBe('www'); + expect(result.CNAME[2].value).toBe('server1'); + }); + + it('should parse reverse zone file with PTR records', () => { + const content = `;; reverse zone file for 127.0.0.1 and ::1 +$TTL 1814400 ; 3 weeks +@ 1814400 IN SOA localhost. root.localhost. ( + 1999010100 ; serial + 10800 ; refresh (3 hours) + 900 ; retry (15 minutes) + 604800 ; expire (1 week) + 86400 ; minimum (1 day) + ) +@ 1814400 IN NS localhost. +1 1814400 IN PTR localhost.`; + + const result = parseDnsRecords(content); + + expect(result.NS).toHaveLength(1); + expect(result.NS[0].name).toBe(''); + expect(result.NS[0].value).toBe('localhost'); + expect(result.NS[0].ttl).toBe(1814400); + + expect(result.PTR).toHaveLength(1); + expect(result.PTR[0].name).toBe('1'); + expect(result.PTR[0].value).toBe('localhost'); + expect(result.PTR[0].ttl).toBe(1814400); + }); + + it('should handle empty content', () => { + const result = parseDnsRecords(''); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); + + it('should handle invalid content', () => { + const result = parseDnsRecords('This is not a valid zone file'); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); + + it('should handle content with only comments', () => { + const result = parseDnsRecords(`; This is a comment +; This is another comment +; And one more comment`); + expect(result.A).toHaveLength(0); + expect(result.AAAA).toHaveLength(0); + expect(result.CNAME).toHaveLength(0); + expect(result.MX).toHaveLength(0); + expect(result.NS).toHaveLength(0); + expect(result.TXT).toHaveLength(0); + }); +}); diff --git a/src/lib/helpers/domains.ts b/src/lib/helpers/domains.ts new file mode 100644 index 000000000..b10a08a82 --- /dev/null +++ b/src/lib/helpers/domains.ts @@ -0,0 +1,256 @@ +import type { DnsRecord } from '$lib/sdk/domains'; + +export type ParsedRecords = { + A: Partial[]; + AAAA: Partial[]; + CNAME: Partial[]; + MX: Partial[]; + TXT: Partial[]; + NS: Partial[]; + SRV: Partial[]; + CAA: Partial[]; + PTR: Partial[]; + HTTPS: Partial[]; + ALIAS: Partial[]; + [key: string]: Partial[]; +}; + +export function parseDnsRecords(content: string): ParsedRecords { + const records: ParsedRecords = { + A: [], + AAAA: [], + CNAME: [], + MX: [], + TXT: [], + NS: [], + SRV: [], + CAA: [], + PTR: [], + HTTPS: [], + ALIAS: [] + }; + + // If content is empty, return empty records + if (!content) { + return records; + } + + // Split the file into lines and process each line + const lines = content.split('\n'); + // Track origin for domain context + let origin = ''; + + for (let line of lines) { + // Skip empty lines and comments + line = line.trim(); + if (line === '' || line.startsWith(';') || line.startsWith('$')) { + // Check for origin directive + if (line.startsWith('$ORIGIN')) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + origin = parts[1]; + // Remove trailing dot if present + if (origin.endsWith('.')) { + origin = origin.slice(0, -1); + } + } + } + continue; + } + + // Skip SOA records + if (line.includes('SOA') || line.match(/^\s*\d+\s*$/)) { + continue; + } + + try { + // Parse the line into tokens, handling quoted values + const tokens: string[] = []; + let currentToken = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"' && (i === 0 || line[i - 1] !== '\\')) { + inQuotes = !inQuotes; + currentToken += char; + } else if (!inQuotes && (char === ' ' || char === '\t') && currentToken) { + tokens.push(currentToken); + currentToken = ''; + // Skip multiple spaces/tabs + while (i + 1 < line.length && (line[i + 1] === ' ' || line[i + 1] === '\t')) { + i++; + } + } else { + currentToken += char; + } + } + + if (currentToken) { + tokens.push(currentToken); + } + + if (tokens.length < 3) { + continue; // Not enough tokens for a valid record + } + + // Find the type index - look for "IN" class and the type after it + let typeIndex = -1; + let type = ''; + let inIndex = -1; // Track position of IN class marker + const validTypes = [ + 'A', + 'AAAA', + 'CNAME', + 'MX', + 'TXT', + 'NS', + 'SRV', + 'CAA', + 'PTR', + 'HTTPS', + 'ALIAS' + ]; + + // First look for IN class followed by record type + for (let i = 0; i < tokens.length - 1; i++) { + if (tokens[i].toUpperCase() === 'IN') { + inIndex = i; + if (validTypes.includes(tokens[i + 1].toUpperCase())) { + typeIndex = i + 1; + type = tokens[i + 1].toUpperCase(); + break; + } + } + } + + // If we didn't find it that way, check for standalone record types + if (typeIndex === -1) { + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i].toUpperCase(); + if (validTypes.includes(token)) { + typeIndex = i; + type = token; + break; + } + } + } + + if (typeIndex === -1 || !type) { + continue; // No valid record type found + } + + // Parse name + let name = tokens[0]; + + // Handle @ symbol for root domain or empty for the domain itself + if (name === '@') { + name = ''; + } + + // Check if name is the same as origin (with or without trailing dot) + const normalizedName = name.endsWith('.') ? name.slice(0, -1) : name; + if (normalizedName === origin) { + name = ''; + } + // For names that end with the origin, extract just the subdomain part + else if (origin && normalizedName.endsWith(origin) && normalizedName !== origin) { + // Handle case like "ns.example.com" where origin is "example.com" + // We want to extract just "ns" + const subdomain = normalizedName.slice(0, -(origin.length + 1)); // +1 for the dot + if (subdomain) { + name = subdomain; + } + } + // Remove domain suffix if present (for absolute names ending with a dot) + else if (name.endsWith('.')) { + name = name.slice(0, -1); + } + + // For cases where first token is IN or a record type, use empty name + if (name.toUpperCase() === 'IN' || validTypes.includes(name.toUpperCase())) { + name = ''; + } + + // Find TTL - it's usually before the IN class + let ttl = 3600; // Default TTL + for (let i = 1; i < typeIndex; i++) { + const possibleTtl = parseInt(tokens[i]); + if (!isNaN(possibleTtl)) { + ttl = possibleTtl; + break; + } + } + + // Create record based on type + const record: Partial = { + name, + ttl, + type + }; + + switch (type) { + case 'A': + case 'AAAA': + case 'CNAME': + case 'NS': + case 'PTR': + case 'HTTPS': + case 'ALIAS': + case 'CAA': + if (typeIndex + 1 >= tokens.length) continue; // Skip if no value + record.value = tokens[typeIndex + 1]; + break; + case 'MX': + if (typeIndex + 2 >= tokens.length) continue; // Skip if not enough tokens + record.priority = parseInt(tokens[typeIndex + 1]) || 10; + record.value = tokens[typeIndex + 2]; + break; + case 'SRV': + if (typeIndex + 4 >= tokens.length) continue; // Skip if not enough tokens + record.priority = parseInt(tokens[typeIndex + 1]) || 0; + record.weight = parseInt(tokens[typeIndex + 2]) || 0; + record.port = parseInt(tokens[typeIndex + 3]) || 0; + record.value = tokens[typeIndex + 4]; + break; + case 'TXT': + // Handle quoted text + const txtValue = tokens.slice(typeIndex + 1).join(' '); + if (!txtValue) continue; // Skip if no value + // Clean quotes if present + record.value = txtValue.replace(/^"(.*)"$/, '$1'); + break; + default: + continue; // Skip unrecognized types + } + + // Clean value - remove trailing dot if present + if (record.value && record.value.endsWith('.')) { + record.value = record.value.slice(0, -1); + } + + // Add the record to the appropriate array if it has a valid value + if (record.value !== undefined && records[type]) { + // For invalid content check - ensure we have something that looks like a domain-related record + // For domain tests - ensure we have at least one of these indicators + const hasValidIndicators = + inIndex !== -1 || // Has 'IN' class + line.includes(' IN ') || + validTypes.some((vt) => line.includes(` ${vt} `)) || + /\d+\.\d+\.\d+\.\d+/.test(line) || // IP address pattern + /\s+NS\s+/.test(line) || // NS record pattern + /\s+MX\s+\d+\s+/.test(line); // MX record pattern + + if (hasValidIndicators) { + records[type].push(record); + } + } + } catch (e) { + console.error('Error parsing line:', line, e); + // Continue to next line + } + } + + return records; +} diff --git a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte index b70d15255..ec2eba302 100644 --- a/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte +++ b/src/routes/(console)/organization-[organization]/domains/domain-[domain]/importRecordModal.svelte @@ -7,135 +7,20 @@ import { IconInfo } from '@appwrite.io/pink-icons-svelte'; import { removeFile } from '$lib/helpers/files'; import { sdk } from '$lib/stores/sdk'; - import type { DnsRecord } from '$lib/sdk/domains'; import { page } from '$app/state'; + import { parseDnsRecords } from '$lib/helpers/domains'; export let show = false; let files: FileList; let error = ''; - type ParsedRecords = { - A: Partial[]; - AAAA: Partial[]; - CNAME: Partial[]; - MX: Partial[]; - TXT: Partial[]; - NS: Partial[]; - SRV: Partial[]; - CAA: Partial[]; - PTR: Partial[]; - HTTPS: Partial[]; - ALIAS: Partial[]; - [key: string]: Partial[]; - }; - - function parseZoneFile(content: string): ParsedRecords { - const records: ParsedRecords = { - A: [], - AAAA: [], - CNAME: [], - MX: [], - TXT: [], - NS: [], - SRV: [], - CAA: [], - PTR: [], - HTTPS: [], - ALIAS: [] - }; - - // Split the content into lines and process each line - const lines = content.split('\n'); - - for (let line of lines) { - // Remove comments, but save them for record comment field - let comment = ''; - const commentIndex = line.indexOf(';'); - if (commentIndex !== -1) { - comment = line.substring(commentIndex + 1).trim(); - line = line.substring(0, commentIndex).trim(); - } - - // Skip empty lines or pure comment lines - if (!line.trim()) continue; - - // Split line into parts (whitespace separated) - const parts = line.trim().split(/\s+/); - - // Need at least a name, TTL/class/type, and value - if (parts.length < 3) continue; - - let recordType = ''; - let name = ''; - let value = ''; - let ttl = 3600; - let priority: number | undefined; - let weight: number | undefined; - let port: number | undefined; - - // Try to determine the record type from the parts - for (let i = 0; i < parts.length; i++) { - const part = parts[i].toUpperCase(); - if (['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'NS', 'SRV', 'CAA'].includes(part)) { - recordType = part; - name = parts[0] === '@' ? '' : parts[0]; // @ represents the domain itself - - // Handle TTL if it appears to be a number - const possibleTtl = parseInt(parts[i - 1]); - if (!isNaN(possibleTtl) && i > 0) { - ttl = possibleTtl; - } - - if (recordType === 'MX' && i + 2 < parts.length) { - priority = parseInt(parts[i + 1]); - value = parts[i + 2]; - } else if (recordType === 'SRV' && i + 4 < parts.length) { - priority = parseInt(parts[i + 1]); - weight = parseInt(parts[i + 2]); - port = parseInt(parts[i + 3]); - value = parts[i + 4]; - } else if (i + 1 < parts.length) { - value = parts.slice(i + 1).join(' '); - } - - break; - } - } - - if (recordType && value) { - if (recordType === 'TXT' && value.startsWith('"') && value.endsWith('"')) { - value = value.slice(1, -1); - } - - const record: Partial = { - name, - value, - ttl, - comment: comment || undefined - }; - - if (priority !== undefined) { - record.priority = priority; - } - - if (!records[recordType]) { - records[recordType] = []; - } - - records[recordType].push(record); - } - } - - return records; - } - async function handleSubmit() { try { if (!files?.length) return; const file = files[0]; const content = await file.text(); - const parsedRecords = parseZoneFile(content); + const parsedRecords = parseDnsRecords(content); let recordCount = 0; From 1d27e2b520ce975c73745c5dfb51846521cf65b6 Mon Sep 17 00:00:00 2001 From: Arman Date: Wed, 2 Apr 2025 16:26:47 +0200 Subject: [PATCH 02/79] feat: make new flow work on selfhosted --- .../(public)/template-[template]/+page.svelte | 19 ++++++------------- .../(public)/template-[template]/+page.ts | 14 ++++++++------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/routes/(public)/template-[template]/+page.svelte b/src/routes/(public)/template-[template]/+page.svelte index 2262fe075..5ec88102b 100644 --- a/src/routes/(public)/template-[template]/+page.svelte +++ b/src/routes/(public)/template-[template]/+page.svelte @@ -5,19 +5,13 @@ import { SvgIcon } from '$lib/components/index.js'; import { Button, Form, InputSelect } from '$lib/elements/forms'; import { getFlagUrl } from '$lib/helpers/flag.js'; - import type { Region } from '$lib/sdk/billing.js'; + import type { AllowedRegions, Region as RegionType } from '$lib/sdk/billing.js'; import { app } from '$lib/stores/app'; import { addNotification } from '$lib/stores/notifications'; import { sdk } from '$lib/stores/sdk'; import { getFrameworkIcon } from '$lib/stores/sites.js'; import { isCloud } from '$lib/system'; - import { - ID, - OAuthProvider, - Query, - type Models, - type Region as AppwriteRegion - } from '@appwrite.io/console'; + import { ID, OAuthProvider, Query, type Models, Region } from '@appwrite.io/console'; import { IconGithub, IconPencil, IconPlus } from '@appwrite.io/pink-icons-svelte'; import { Card, @@ -39,8 +33,8 @@ ); let projectName = $state(); let showCustomId = $state(false); - let region = $state(); - let regions = $state>([]); + let region = $state(); + let regions = $state>([]); let id = $state(); function getRegions() { @@ -101,7 +95,7 @@ id ?? ID.unique(), projectName, selectedOrg, - region as AppwriteRegion + isCloud ? (region as Region) : Region.Default ); trackEvent(Submit.ProjectCreate, { customId: !!id, @@ -124,7 +118,6 @@ $effect(() => { if (selectedOrg !== undefined) { - console.log('test'); fetchProjects(); } }); @@ -240,7 +233,7 @@ isProject bind:id /> - {#if isCloud && regions.length > 0} + {#if isCloud} { - if (!isCloud) redirect(303, '/'); - const { account } = await parent(); if (!url.searchParams.has('type')) { @@ -27,10 +26,13 @@ export const load = async ({ parent, url, params }) => { default: error(404, 'Type is not valid'); } - const organizations = account?.$id - ? await sdk.forConsole.billing.listOrganization() - : undefined; - // const organizations = account?.$id ? await sdk.forConsole.teams.list() : undefined; + + let organizations: Models.TeamList> | OrganizationList | undefined; + if (isCloud) { + organizations = account?.$id ? await sdk.forConsole.billing.listOrganization() : undefined; + } else { + organizations = account?.$id ? await sdk.forConsole.teams.list() : undefined; + } if (!organizations?.total && account?.$id) { await sdk.forConsole.billing.createOrganization( From 42773185572b03bb451576950aa5e6e7527c6af9 Mon Sep 17 00:00:00 2001 From: Arman Date: Wed, 2 Apr 2025 16:26:59 +0200 Subject: [PATCH 03/79] chore: remove imports --- src/routes/(public)/template-[template]/+page.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/(public)/template-[template]/+page.ts b/src/routes/(public)/template-[template]/+page.ts index 473ba7df7..937031a41 100644 --- a/src/routes/(public)/template-[template]/+page.ts +++ b/src/routes/(public)/template-[template]/+page.ts @@ -2,7 +2,6 @@ import { BillingPlan } from '$lib/constants.js'; import { sdk } from '$lib/stores/sdk.js'; import { ID, type Models } from '@appwrite.io/console'; import { isCloud } from '$lib/system.js'; -import { redirect } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; import type { OrganizationList } from '$lib/stores/organization.js'; From de76e7ef86b4088b19e17ecaed6d58f06c70807d Mon Sep 17 00:00:00 2001 From: Arman Date: Wed, 2 Apr 2025 16:31:28 +0200 Subject: [PATCH 04/79] fix: stop copy from propagating --- src/lib/components/copy.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/copy.svelte b/src/lib/components/copy.svelte index 1f21d2e3e..970d04074 100644 --- a/src/lib/components/copy.svelte +++ b/src/lib/components/copy.svelte @@ -40,7 +40,7 @@ role="button" tabindex="0" style:cursor="pointer" - on:click|preventDefault={handleClick} + on:click|preventDefault|stopPropagation={handleClick} on:keyup={clickOnEnter} on:mouseenter={() => setTimeout(() => (content = 'Click to copy'))}> From 00bc495d735c274bf8a8b74cef9a25c9956f594c Mon Sep 17 00:00:00 2001 From: Arman Date: Wed, 2 Apr 2025 16:38:40 +0200 Subject: [PATCH 05/79] fix: logs table --- .../project-[project]/sites/site-[site]/logs/store.ts | 4 ---- .../project-[project]/sites/site-[site]/logs/table.svelte | 8 +++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/logs/store.ts b/src/routes/(console)/project-[project]/sites/site-[site]/logs/store.ts index 074f38cf5..e70b57452 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/logs/store.ts +++ b/src/routes/(console)/project-[project]/sites/site-[site]/logs/store.ts @@ -16,7 +16,6 @@ export const columns = writable([ title: 'Trigger', type: 'string', exclude: true, - width: 90, array: true, format: 'enum', elements: [ @@ -31,7 +30,6 @@ export const columns = writable([ title: 'Method', type: 'string', exclude: true, - width: 70, array: true, format: 'enum', elements: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] @@ -41,7 +39,6 @@ export const columns = writable([ title: 'Status code', type: 'integer', exclude: true, - width: 100, format: 'integer', elements: [ { @@ -68,7 +65,6 @@ export const columns = writable([ id: 'duration', title: 'Duration', type: 'integer', - width: 80, format: 'integer', elements: [ { diff --git a/src/routes/(console)/project-[project]/sites/site-[site]/logs/table.svelte b/src/routes/(console)/project-[project]/sites/site-[site]/logs/table.svelte index 573c61759..fc953bdfb 100644 --- a/src/routes/(console)/project-[project]/sites/site-[site]/logs/table.svelte +++ b/src/routes/(console)/project-[project]/sites/site-[site]/logs/table.svelte @@ -11,11 +11,13 @@ let openSheet = false; let selectedLogId: string = null; + + $: filteredColumns = columns.filter((c) => !c.exclude); - + - {#each columns.filter((c) => !c.exclude) as { id, title }} + {#each filteredColumns as { id, title }} {title} {/each} @@ -26,7 +28,7 @@ openSheet = true; selectedLogId = log.$id; }}> - {#each columns.filter((c) => !c.exclude) as column} + {#each filteredColumns as column} {#if column.id === '$id'} {#key column.id} From a05125469478d2add57f7a8ce4664e5757096dbf Mon Sep 17 00:00:00 2001 From: Arman Date: Wed, 2 Apr 2025 16:49:20 +0200 Subject: [PATCH 06/79] fix: wizard footer zindex --- .../sites/create-site/deploying/+page.svelte | 2 +- .../sites/create-site/finish/+page.svelte | 169 +++++++++--------- 2 files changed, 84 insertions(+), 87 deletions(-) diff --git a/src/routes/(console)/project-[project]/sites/create-site/deploying/+page.svelte b/src/routes/(console)/project-[project]/sites/create-site/deploying/+page.svelte index 57faea607..791e03a0c 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/deploying/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/deploying/+page.svelte @@ -23,7 +23,7 @@ async (response: RealtimeResponseEvent) => { if ( response.events.includes( - `sites.${data.deployment.resourceId}.deployments.${data.deployment.$id}.update` + `sites.${data.site.$id}.deployments.${data.deployment.$id}.update` ) ) { deployment = response.payload; diff --git a/src/routes/(console)/project-[project]/sites/create-site/finish/+page.svelte b/src/routes/(console)/project-[project]/sites/create-site/finish/+page.svelte index 0f1ba652b..de3e46343 100644 --- a/src/routes/(console)/project-[project]/sites/create-site/finish/+page.svelte +++ b/src/routes/(console)/project-[project]/sites/create-site/finish/+page.svelte @@ -34,8 +34,8 @@ -
- + +
@@ -57,110 +57,107 @@ +
- -
+ +
- - - - - {#if !data.site.providerRepositoryId} - (showConnectRepositry = true)}> - - - - Add repository - - - - - Connect to a new repository or an existing one. - - - - {/if} - { - trackEvent(Click.DomainCreateClick, { - source: 'sites_create_finish' - }); - }} - href={`${base}/project-${page.params.project}/sites/site-${data.site.$id}/domains`}> - - - Add domain - - - - Connect to an existing domain or add a new one. - - - + + + + + {#if !data.site.providerRepositoryId} (showInviteCollaborator = true)}> + on:click={() => (showConnectRepositry = true)}> - Share site + Add repository - Share your progress and start collaborating with your team. + Connect to a new repository or an existing one. - (showOpenOnMobile = true)}> - - - Open on mobile - - - - Open the preview of your site on any mobile or tablet - device. - + {/if} + { + trackEvent(Click.DomainCreateClick, { + source: 'sites_create_finish' + }); + }} + href={`${base}/project-${page.params.project}/sites/site-${data.site.$id}/domains`}> + + + Add domain + - - - - - - -
+ + Connect to an existing domain or add a new one. + +
+ + (showInviteCollaborator = true)}> + + + Share site + + + + Share your progress and start collaborating with your team. + + + + (showOpenOnMobile = true)}> + + + Open on mobile + + + + Open the preview of your site on any mobile or tablet device. + + + + + + + + + + {tooltipMessage} + + - QR code + QR code - - - copy(url)} /> - - diff --git a/src/routes/(console)/project-[project]/sites/(components)/siteCard.svelte b/src/routes/(console)/project-[project]/sites/(components)/siteCard.svelte index 98d5d07ab..e58a02ae8 100644 --- a/src/routes/(console)/project-[project]/sites/(components)/siteCard.svelte +++ b/src/routes/(console)/project-[project]/sites/(components)/siteCard.svelte @@ -186,8 +186,8 @@ -{#if show && siteUrl} - +{#if show && proxyRuleList.total} + {/if}