DNS server (add NS record)
This commit is contained in:
@@ -116,6 +116,7 @@
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="TXT">TXT</option>
|
||||
<option value="NS">NS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
@@ -125,6 +126,13 @@
|
||||
<div style="form-group">
|
||||
<p>Value:</p>
|
||||
<input id="record-value" type="text" style="form-input" placeholder="192.168.1.1, example.com" />
|
||||
<div id="record-help" style="text-[#6b7280] text-sm mt-1">
|
||||
<span id="help-A">Enter an IPv4 address (e.g., 192.168.1.1)</span>
|
||||
<span id="help-AAAA" style="hidden">Enter an IPv6 address (e.g., 2001:db8::1)</span>
|
||||
<span id="help-CNAME" style="hidden">Enter a domain name (e.g., example.com)</span>
|
||||
<span id="help-TXT" style="hidden">Enter any text content</span>
|
||||
<span id="help-NS" style="hidden">Enter a nameserver domain (e.g., ns1.example.com)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>TTL:</p>
|
||||
|
||||
@@ -92,10 +92,16 @@ renderRecords = function(appendOnly)
|
||||
if #records == 0 then
|
||||
local emptyMessage = gurt.create('div', {
|
||||
text = 'No DNS records found. Add your first record below!',
|
||||
style = 'text-center text-[#6b7280] py-8'
|
||||
style = 'text-center text-[#6b7280] py-8',
|
||||
id = '404'
|
||||
})
|
||||
recordsList:append(emptyMessage)
|
||||
return
|
||||
else
|
||||
local err = gurt.select('#404')
|
||||
if err then
|
||||
gurt.select('#404'):remove()
|
||||
end
|
||||
end
|
||||
|
||||
-- Create header only if not appending or if list is empty
|
||||
@@ -222,6 +228,11 @@ end
|
||||
local function addRecord(type, name, value, ttl)
|
||||
hideError('record-error')
|
||||
print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
|
||||
print('Network request details:')
|
||||
print(' URL: gurt://localhost:8877/domain/' .. domainName .. '/records')
|
||||
print(' Method: POST')
|
||||
print(' Auth token: ' .. (authToken and 'present' or 'missing'))
|
||||
print(' Domain name: ' .. (domainName or 'nil'))
|
||||
|
||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||
method = 'POST',
|
||||
@@ -237,30 +248,51 @@ local function addRecord(type, name, value, ttl)
|
||||
})
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
print('DNS record added successfully')
|
||||
print('Response received: ' .. tostring(response))
|
||||
|
||||
if response then
|
||||
print('Response status: ' .. tostring(response.status))
|
||||
print('Response ok: ' .. tostring(response:ok()))
|
||||
|
||||
-- Clear form
|
||||
gurt.select('#record-name').value = ''
|
||||
gurt.select('#record-value').value = ''
|
||||
gurt.select('#record-ttl').value = '3600'
|
||||
|
||||
-- Add the new record to existing records array
|
||||
local newRecord = response:json()
|
||||
if newRecord and newRecord.id then
|
||||
-- Server returned the created record, add it to our local array
|
||||
table.insert(records, newRecord)
|
||||
-- Render only the new record
|
||||
renderRecords(true)
|
||||
if response:ok() then
|
||||
print('DNS record added successfully')
|
||||
|
||||
-- Clear form
|
||||
gurt.select('#record-name').value = ''
|
||||
gurt.select('#record-value').value = ''
|
||||
gurt.select('#record-ttl').value = '3600'
|
||||
|
||||
-- Add the new record to existing records array
|
||||
local newRecord = response:json()
|
||||
if newRecord and newRecord.id then
|
||||
-- Server returned the created record, add it to our local array
|
||||
table.insert(records, newRecord)
|
||||
|
||||
-- Check if we had no records before (showing empty message)
|
||||
local wasEmpty = (#records == 1) -- If this is the first record
|
||||
|
||||
if wasEmpty then
|
||||
-- Full re-render to replace empty message with proper table
|
||||
renderRecords(false)
|
||||
else
|
||||
-- Just append the new record to existing table
|
||||
renderRecords(true)
|
||||
end
|
||||
else
|
||||
-- Server didn't return record details, reload to get the actual data
|
||||
loadRecords()
|
||||
end
|
||||
else
|
||||
-- Server didn't return record details, reload to get the actual data
|
||||
loadRecords()
|
||||
local error = response:text()
|
||||
showError('record-error', 'Failed to add record: ' .. error)
|
||||
print('Failed to add DNS record: ' .. error)
|
||||
end
|
||||
else
|
||||
local error = response:text()
|
||||
showError('record-error', 'Failed to add record: ' .. error)
|
||||
print('Failed to add DNS record: ' .. error)
|
||||
print('No response received from server')
|
||||
showError('record-error', 'No response from server - connection failed')
|
||||
print('Failed to add DNS record: No response')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
local function logout()
|
||||
@@ -273,9 +305,42 @@ local function goBack()
|
||||
--gurt.location.goto("/dashboard.html")
|
||||
end
|
||||
|
||||
-- Function to update help text based on record type
|
||||
local function updateHelpText()
|
||||
local recordType = gurt.select('#record-type').value
|
||||
|
||||
-- Hide all help texts
|
||||
local helpTypes = {'A', 'AAAA', 'CNAME', 'TXT', 'NS'}
|
||||
for _, helpType in ipairs(helpTypes) do
|
||||
local helpElement = gurt.select('#help-' .. helpType)
|
||||
if helpElement then
|
||||
helpElement.classList:add('hidden')
|
||||
end
|
||||
end
|
||||
|
||||
-- Show the relevant help text
|
||||
local currentHelp = gurt.select('#help-' .. recordType)
|
||||
if currentHelp then
|
||||
currentHelp.classList:remove('hidden')
|
||||
end
|
||||
|
||||
-- Update placeholder text based on record type
|
||||
local valueInput = gurt.select('#record-value')
|
||||
if recordType == 'A' then
|
||||
valueInput.placeholder = '192.168.1.1'
|
||||
elseif recordType == 'AAAA' then
|
||||
valueInput.placeholder = '2001:db8::1'
|
||||
elseif recordType == 'CNAME' or recordType == 'NS' then
|
||||
valueInput.placeholder = 'example.com'
|
||||
elseif recordType == 'TXT' then
|
||||
valueInput.placeholder = 'Any text content'
|
||||
end
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
gurt.select('#logout-btn'):on('click', logout)
|
||||
gurt.select('#back-btn'):on('click', goBack)
|
||||
gurt.select('#record-type'):on('change', updateHelpText)
|
||||
|
||||
gurt.select('#add-record-btn'):on('click', function()
|
||||
local recordType = gurt.select('#record-type').value
|
||||
@@ -297,4 +362,5 @@ end)
|
||||
|
||||
-- Initialize
|
||||
print('Domain management page initialized')
|
||||
updateHelpText() -- Set initial help text
|
||||
checkAuth()
|
||||
|
||||
@@ -7,35 +7,33 @@
|
||||
<font name="roboto" src="https://fonts.gstatic.com/s/roboto/v48/KFO7CnqEu92Fr1ME7kSn66aGLdTylUAMa3KUBGEe.woff2" />
|
||||
|
||||
<style>
|
||||
body { bg-[#2a2a2a] text-[#ffffff] font-roboto flex items-center justify-center p-4 }
|
||||
.login-card { p-8 }
|
||||
h1 { text-3xl font-bold text-center mb-6 text-[#ffffff] }
|
||||
body {
|
||||
bg-[#171616] font-sans text-white
|
||||
}
|
||||
|
||||
.login-card {
|
||||
bg-[#262626] p-8 rounded-lg shadow-lg max-w-md mx-auto my-auto h-full
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-3xl font-bold text-center mb-6
|
||||
}
|
||||
|
||||
input {
|
||||
bg-[#3b3b3b]
|
||||
border-none
|
||||
rounded-md
|
||||
p-3
|
||||
w-full
|
||||
text-[#ffffff]
|
||||
placeholder:text-[#999999]
|
||||
outline-none
|
||||
focus:ring-2
|
||||
focus:ring-[#5b5b5b]
|
||||
mb-4
|
||||
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white mb-4 placeholder:text-[#999999] outline-none active:border-red-500
|
||||
}
|
||||
|
||||
button {
|
||||
bg-[#4ade80]
|
||||
text-[#1b1b1b]
|
||||
font-bold
|
||||
p-3
|
||||
rounded-md
|
||||
w-full
|
||||
hover:bg-[#22c55e]
|
||||
active:bg-[#15803d]
|
||||
cursor-pointer
|
||||
bg-[#dc2626] text-white font-medium p-3 rounded-lg w-full cursor-pointer transition-colors hover:bg-[#b91c1c] active:bg-[#991b1b]
|
||||
}
|
||||
|
||||
a {
|
||||
text-[#ef4444] hover:text-[#dc2626] cursor-pointer
|
||||
}
|
||||
|
||||
#log-output {
|
||||
text-[#fca5a5] p-4 rounded-md mt-4 font-mono max-h-40
|
||||
}
|
||||
a { text-[#4ade80] hover:text-[#22c55e] cursor-pointer }
|
||||
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
||||
</style>
|
||||
|
||||
<script src="script.lua" />
|
||||
|
||||
@@ -105,10 +105,7 @@
|
||||
<p id="tld-loading">Loading TLDs...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>IP Address:</p>
|
||||
<input id="domain-ip" type="text" style="form-input" placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<p style="text-[#6b7280] text-sm mb-4">Note: After registration is approved, you can add DNS records (A, AAAA, CNAME, TXT) to configure where your domain points.</p>
|
||||
<div id="domain-error" style="error-text hidden mb-2"></div>
|
||||
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
||||
</div>
|
||||
|
||||
@@ -122,7 +122,7 @@ local function goToDashboard()
|
||||
gurt.location.goto("/dashboard.html")
|
||||
end
|
||||
|
||||
local function submitDomain(name, tld, ip)
|
||||
local function submitDomain(name, tld)
|
||||
hideError('domain-error')
|
||||
print('Submitting domain: ' .. name .. '.' .. tld)
|
||||
|
||||
@@ -132,7 +132,7 @@ local function submitDomain(name, tld, ip)
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({ name = name, tld = tld, ip = ip })
|
||||
body = JSON.stringify({ name = name, tld = tld })
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
@@ -144,7 +144,6 @@ local function submitDomain(name, tld, ip)
|
||||
|
||||
-- Clear form
|
||||
gurt.select('#domain-name').text = ''
|
||||
gurt.select('#domain-ip').text = ''
|
||||
|
||||
-- Redirect to dashboard
|
||||
gurt.location.goto('/dashboard.html')
|
||||
@@ -212,12 +211,10 @@ gurt.select('#dashboard-btn'):on('click', goToDashboard)
|
||||
|
||||
gurt.select('#submit-domain-btn'):on('click', function()
|
||||
local name = gurt.select('#domain-name').value
|
||||
local ip = gurt.select('#domain-ip').value
|
||||
local selectedTLD = gurt.select('.tld-selected')
|
||||
|
||||
print('Submit domain button clicked')
|
||||
print('Input name:', name)
|
||||
print('Input IP:', ip)
|
||||
print('Selected TLD element:', selectedTLD)
|
||||
|
||||
if not name or name == '' then
|
||||
@@ -226,12 +223,6 @@ gurt.select('#submit-domain-btn'):on('click', function()
|
||||
return
|
||||
end
|
||||
|
||||
if not ip or ip == '' then
|
||||
print('Validation failed: IP address is required')
|
||||
showError('domain-error', 'IP address is required')
|
||||
return
|
||||
end
|
||||
|
||||
if not selectedTLD then
|
||||
print('Validation failed: No TLD selected')
|
||||
showError('domain-error', 'Please select a TLD')
|
||||
@@ -239,8 +230,8 @@ gurt.select('#submit-domain-btn'):on('click', function()
|
||||
end
|
||||
|
||||
local tld = selectedTLD:getAttribute('data-tld')
|
||||
print('Submitting domain with name:', name, 'tld:', tld, 'ip:', ip)
|
||||
submitDomain(name, tld, ip)
|
||||
print('Submitting domain with name:', name, 'tld:', tld)
|
||||
submitDomain(name, tld)
|
||||
end)
|
||||
|
||||
gurt.select('#create-invite-btn'):on('click', createInvite)
|
||||
|
||||
7
dns/migrations/002_remove_ip_requirement.sql
Normal file
7
dns/migrations/002_remove_ip_requirement.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Make IP column optional for domains
|
||||
ALTER TABLE domains ALTER COLUMN ip DROP NOT NULL;
|
||||
|
||||
-- Update DNS records constraint to only allow A, AAAA, CNAME, TXT
|
||||
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
|
||||
ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
|
||||
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT'));
|
||||
10
dns/migrations/003_add_ns_records.sql
Normal file
10
dns/migrations/003_add_ns_records.sql
Normal file
@@ -0,0 +1,10 @@
|
||||
-- Re-add NS record support and extend record types
|
||||
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
|
||||
ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
|
||||
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS', 'MX'));
|
||||
|
||||
-- Add index for efficient NS record lookups during delegation
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS';
|
||||
|
||||
-- Add index for subdomain resolution optimization
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);
|
||||
8
dns/migrations/004_fix_record_types.sql
Normal file
8
dns/migrations/004_fix_record_types.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Fix record types to remove MX and ensure NS is supported
|
||||
ALTER TABLE dns_records DROP CONSTRAINT IF EXISTS dns_records_record_type_check;
|
||||
ALTER TABLE dns_records ADD CONSTRAINT dns_records_record_type_check
|
||||
CHECK (record_type IN ('A', 'AAAA', 'CNAME', 'TXT', 'NS'));
|
||||
|
||||
-- Add indexes for efficient DNS lookups if they don't exist
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_ns_lookup ON dns_records(record_type, name) WHERE record_type = 'NS';
|
||||
CREATE INDEX IF NOT EXISTS idx_dns_records_subdomain_lookup ON dns_records(domain_id, name, record_type);
|
||||
@@ -2,16 +2,11 @@ use serenity::async_trait;
|
||||
use serenity::all::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub struct DiscordBot {
|
||||
pub pool: PgPool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DomainRegistration {
|
||||
pub id: i32,
|
||||
pub domain_name: String,
|
||||
pub tld: String,
|
||||
pub ip: String,
|
||||
pub user_id: i32,
|
||||
pub username: String,
|
||||
}
|
||||
@@ -40,31 +35,61 @@ impl EventHandler for BotHandler {
|
||||
}
|
||||
};
|
||||
|
||||
// Update domain status to approved
|
||||
match sqlx::query("UPDATE domains SET status = 'approved' WHERE id = $1")
|
||||
.bind(domain_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("✅ Domain approved!")
|
||||
.ephemeral(true)
|
||||
);
|
||||
|
||||
if let Err(e) = component.create_response(&ctx.http, response).await {
|
||||
log::error!("Error responding to interaction: {}", e);
|
||||
// Get domain info for the updated embed
|
||||
let domain: Option<(String, String, String)> = sqlx::query_as(
|
||||
"SELECT d.name, d.tld, u.username FROM domains d JOIN users u ON d.user_id = u.id WHERE d.id = $1"
|
||||
)
|
||||
.bind(domain_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some((name, tld, username)) = domain {
|
||||
// Update domain status to approved
|
||||
match sqlx::query("UPDATE domains SET status = 'approved' WHERE id = $1")
|
||||
.bind(domain_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// First, send ephemeral confirmation
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("✅ Domain approved!")
|
||||
.ephemeral(true)
|
||||
);
|
||||
|
||||
if let Err(e) = component.create_response(&ctx.http, response).await {
|
||||
log::error!("Error responding to interaction: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then edit the original message with green color and no buttons
|
||||
let updated_embed = CreateEmbed::new()
|
||||
.title("✅ Domain Registration - APPROVED")
|
||||
.field("Domain", format!("{}.{}", name, tld), true)
|
||||
.field("User", username, true)
|
||||
.field("Status", "Approved", true)
|
||||
.color(0x00ff00); // Green color
|
||||
|
||||
let edit_message = EditMessage::new()
|
||||
.embed(updated_embed)
|
||||
.components(vec![]); // Remove buttons
|
||||
|
||||
let mut message = component.message.clone();
|
||||
if let Err(e) = message.edit(&ctx.http, edit_message).await {
|
||||
log::error!("Error updating original message: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error approving domain: {}", e);
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Error approving domain")
|
||||
.ephemeral(true)
|
||||
);
|
||||
let _ = component.create_response(&ctx.http, response).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error approving domain: {}", e);
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Error approving domain")
|
||||
.ephemeral(true)
|
||||
);
|
||||
let _ = component.create_response(&ctx.http, response).await;
|
||||
}
|
||||
}
|
||||
} else if custom_id.starts_with("deny_") {
|
||||
@@ -116,32 +141,66 @@ impl EventHandler for BotHandler {
|
||||
})
|
||||
.unwrap_or("No reason provided");
|
||||
|
||||
// Update domain status to denied with reason
|
||||
match sqlx::query("UPDATE domains SET status = 'denied', denial_reason = $1 WHERE id = $2")
|
||||
.bind(reason)
|
||||
.bind(domain_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Domain denied!")
|
||||
.ephemeral(true)
|
||||
);
|
||||
|
||||
if let Err(e) = modal_submit.create_response(&ctx.http, response).await {
|
||||
log::error!("Error responding to modal: {}", e);
|
||||
// Get domain info for the updated embed
|
||||
let domain: Option<(String, String, String)> = sqlx::query_as(
|
||||
"SELECT d.name, d.tld, u.username FROM domains d JOIN users u ON d.user_id = u.id WHERE d.id = $1"
|
||||
)
|
||||
.bind(domain_id)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
if let Some((name, tld, username)) = domain {
|
||||
// Update domain status to denied with reason
|
||||
match sqlx::query("UPDATE domains SET status = 'denied', denial_reason = $1 WHERE id = $2")
|
||||
.bind(reason)
|
||||
.bind(domain_id)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// First, send ephemeral confirmation
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Domain denied!")
|
||||
.ephemeral(true)
|
||||
);
|
||||
|
||||
if let Err(e) = modal_submit.create_response(&ctx.http, response).await {
|
||||
log::error!("Error responding to modal: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then edit the original message with red color and no buttons
|
||||
let updated_embed = CreateEmbed::new()
|
||||
.title("❌ Domain Registration - DENIED")
|
||||
.field("Domain", format!("{}.{}", name, tld), true)
|
||||
.field("User", username, true)
|
||||
.field("Status", "Denied", true)
|
||||
.field("Reason", reason, false)
|
||||
.color(0xff0000); // Red color
|
||||
|
||||
let edit_message = EditMessage::new()
|
||||
.embed(updated_embed)
|
||||
.components(vec![]); // Remove buttons
|
||||
|
||||
if let Some(mut message) = modal_submit.message.clone() {
|
||||
if let Err(e) = message.edit(&ctx.http, edit_message).await {
|
||||
log::error!("Error updating original message: {}", e);
|
||||
}
|
||||
} else {
|
||||
log::error!("Original message not found for editing");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error denying domain: {}", e);
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Error denying domain")
|
||||
.ephemeral(true)
|
||||
);
|
||||
let _ = modal_submit.create_response(&ctx.http, response).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Error denying domain: {}", e);
|
||||
let response = CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("❌ Error denying domain")
|
||||
.ephemeral(true)
|
||||
);
|
||||
let _ = modal_submit.create_response(&ctx.http, response).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,12 +221,12 @@ pub async fn send_domain_approval_request(
|
||||
let http = serenity::http::Http::new(bot_token);
|
||||
|
||||
let embed = CreateEmbed::new()
|
||||
.title("New Domain Registration")
|
||||
.title("Domain request")
|
||||
.field("Domain", format!("{}.{}", registration.domain_name, registration.tld), true)
|
||||
.field("IP", ®istration.ip, true)
|
||||
.field("User", ®istration.username, true)
|
||||
.field("User ID", registration.user_id.to_string(), true)
|
||||
.color(0x00ff00);
|
||||
.field("Status", "Pending Review", true)
|
||||
.color(0x808080); // Gray color for pending
|
||||
|
||||
let approve_button = CreateButton::new(format!("approve_{}", registration.id))
|
||||
.style(ButtonStyle::Success)
|
||||
|
||||
@@ -95,6 +95,8 @@ enum HandlerType {
|
||||
DeleteDomain,
|
||||
GetUserDomains,
|
||||
CreateDomainRecord,
|
||||
ResolveDomain,
|
||||
ResolveFullDomain,
|
||||
}
|
||||
|
||||
impl GurtHandler for AppHandler {
|
||||
@@ -163,6 +165,8 @@ impl GurtHandler for AppHandler {
|
||||
handle_authenticated!(ctx, app_state, routes::delete_domain)
|
||||
}
|
||||
},
|
||||
HandlerType::ResolveDomain => routes::resolve_domain(&ctx, app_state).await,
|
||||
HandlerType::ResolveFullDomain => routes::resolve_full_domain(&ctx, app_state).await,
|
||||
};
|
||||
|
||||
let duration = start_time.elapsed();
|
||||
@@ -231,7 +235,9 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
||||
.route(Route::get("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomain })
|
||||
.route(Route::post("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainRecord })
|
||||
.route(Route::put("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::UpdateDomain })
|
||||
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain });
|
||||
.route(Route::delete("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::DeleteDomain })
|
||||
.route(Route::post("/resolve"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveDomain })
|
||||
.route(Route::post("/resolve-full"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::ResolveFullDomain });
|
||||
|
||||
log::info!("GURT server listening on {}", config.get_address());
|
||||
server.listen(&config.get_address()).await.map_err(|e| {
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
use gurt::prelude::*;
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub fn validate_ip(domain: &super::models::Domain) -> Result<()> {
|
||||
if domain.ip.parse::<IpAddr>().is_err() {
|
||||
return Err(GurtError::invalid_message("Invalid IP address"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn deserialize_lowercase<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
|
||||
where
|
||||
|
||||
@@ -6,11 +6,12 @@ use sqlx::{FromRow, types::chrono::{DateTime, Utc}};
|
||||
pub struct Domain {
|
||||
#[serde(skip_deserializing)]
|
||||
pub(crate) id: Option<i32>,
|
||||
pub(crate) ip: String,
|
||||
#[serde(deserialize_with = "deserialize_lowercase")]
|
||||
pub(crate) tld: String,
|
||||
#[serde(deserialize_with = "deserialize_lowercase")]
|
||||
pub(crate) name: String,
|
||||
#[serde(deserialize_with = "deserialize_lowercase")]
|
||||
pub(crate) tld: String,
|
||||
#[serde(skip_deserializing)]
|
||||
pub(crate) ip: Option<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
pub(crate) user_id: Option<i32>,
|
||||
#[serde(skip_deserializing)]
|
||||
@@ -70,7 +71,6 @@ pub struct DomainInviteCode {
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ResponseDomain {
|
||||
pub(crate) tld: String,
|
||||
pub(crate) ip: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) records: Option<Vec<ResponseDnsRecord>>,
|
||||
}
|
||||
@@ -87,8 +87,16 @@ pub(crate) struct ResponseDnsRecord {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct UpdateDomain {
|
||||
pub(crate) ip: String,
|
||||
pub(crate) struct DnsResolutionRequest {
|
||||
pub(crate) name: String,
|
||||
pub(crate) tld: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct DnsResolutionResponse {
|
||||
pub(crate) name: String,
|
||||
pub(crate) tld: String,
|
||||
pub(crate) records: Vec<ResponseDnsRecord>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -108,7 +116,6 @@ pub(crate) struct DomainList {
|
||||
pub(crate) struct UserDomain {
|
||||
pub(crate) name: String,
|
||||
pub(crate) tld: String,
|
||||
pub(crate) ip: String,
|
||||
pub(crate) status: String,
|
||||
pub(crate) denial_reason: Option<String>,
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::{models::*, AppState, helpers::validate_ip};
|
||||
use super::{models::*, AppState};
|
||||
use crate::auth::Claims;
|
||||
use crate::discord_bot::{send_domain_approval_request, DomainRegistration};
|
||||
use gurt::prelude::*;
|
||||
use std::{env, collections::HashMap};
|
||||
|
||||
@@ -23,8 +24,6 @@ pub(crate) async fn index(_app_state: AppState) -> Result<GurtResponse> {
|
||||
}
|
||||
|
||||
pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -> Result<Domain> {
|
||||
validate_ip(&domain)?;
|
||||
|
||||
if !app.config.tld_list().contains(&domain.tld.as_str())
|
||||
|| !domain.name.chars().all(|c| c.is_alphabetic() || c == '-')
|
||||
|| domain.name.len() > 24
|
||||
@@ -51,17 +50,26 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|
||||
return Err(GurtError::invalid_message("Domain already exists"));
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO domains (name, tld, ip, user_id, status) VALUES ($1, $2, $3, $4, 'pending')"
|
||||
let user: (String,) = sqlx::query_as("SELECT username FROM users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_one(&app.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("User not found"))?;
|
||||
|
||||
let username = user.0;
|
||||
|
||||
let domain_row: (i32,) = sqlx::query_as(
|
||||
"INSERT INTO domains (name, tld, user_id, status) VALUES ($1, $2, $3, 'pending') RETURNING id"
|
||||
)
|
||||
.bind(&domain.name)
|
||||
.bind(&domain.tld)
|
||||
.bind(&domain.ip)
|
||||
.bind(user_id)
|
||||
.execute(&app.db)
|
||||
.fetch_one(&app.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Failed to create domain"))?;
|
||||
|
||||
let domain_id = domain_row.0;
|
||||
|
||||
// Decrease user's registrations remaining
|
||||
sqlx::query("UPDATE users SET registrations_remaining = registrations_remaining - 1 WHERE id = $1")
|
||||
.bind(user_id)
|
||||
@@ -69,6 +77,29 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Failed to update user registrations"))?;
|
||||
|
||||
if !app.config.discord.bot_token.is_empty() && app.config.discord.channel_id != 0 {
|
||||
let domain_registration = DomainRegistration {
|
||||
id: domain_id,
|
||||
domain_name: domain.name.clone(),
|
||||
tld: domain.tld.clone(),
|
||||
user_id,
|
||||
username: username.clone(),
|
||||
};
|
||||
|
||||
let channel_id = app.config.discord.channel_id;
|
||||
let bot_token = app.config.discord.bot_token.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_domain_approval_request(
|
||||
channel_id,
|
||||
domain_registration,
|
||||
&bot_token,
|
||||
).await {
|
||||
log::error!("Failed to send Discord notification: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(domain)
|
||||
}
|
||||
|
||||
@@ -177,7 +208,6 @@ pub(crate) async fn get_domains(ctx: &ServerContext, app_state: AppState) -> Res
|
||||
ResponseDomain {
|
||||
name: domain.name,
|
||||
tld: domain.tld,
|
||||
ip: domain.ip,
|
||||
records: None,
|
||||
}
|
||||
}).collect();
|
||||
@@ -226,56 +256,8 @@ pub(crate) async fn check_domain(ctx: &ServerContext, app_state: AppState) -> Re
|
||||
Ok(GurtResponse::ok().with_json_body(&domain_list)?)
|
||||
}
|
||||
|
||||
pub(crate) async fn update_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||
if path_parts.len() < 4 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{name}/{tld}"));
|
||||
}
|
||||
|
||||
let name = path_parts[2];
|
||||
let tld = path_parts[3];
|
||||
|
||||
let update_data: UpdateDomain = serde_json::from_slice(ctx.body())
|
||||
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
|
||||
|
||||
// Verify user owns this domain
|
||||
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND user_id = $3"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(tld)
|
||||
.bind(claims.user_id)
|
||||
.fetch_optional(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
let domain = match domain {
|
||||
Some(d) => d,
|
||||
None => return Ok(GurtResponse::not_found().with_string_body("Domain not found or access denied"))
|
||||
};
|
||||
|
||||
// Validate IP
|
||||
validate_ip(&Domain {
|
||||
id: domain.id,
|
||||
name: domain.name.clone(),
|
||||
tld: domain.tld.clone(),
|
||||
ip: update_data.ip.clone(),
|
||||
user_id: domain.user_id,
|
||||
status: domain.status,
|
||||
denial_reason: domain.denial_reason,
|
||||
created_at: domain.created_at,
|
||||
})?;
|
||||
|
||||
sqlx::query("UPDATE domains SET ip = $1 WHERE name = $2 AND tld = $3 AND user_id = $4")
|
||||
.bind(&update_data.ip)
|
||||
.bind(name)
|
||||
.bind(tld)
|
||||
.bind(claims.user_id)
|
||||
.execute(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Failed to update domain"))?;
|
||||
|
||||
Ok(GurtResponse::ok().with_string_body("Domain updated successfully"))
|
||||
pub(crate) async fn update_domain(_ctx: &ServerContext, _app_state: AppState, _claims: Claims) -> Result<GurtResponse> {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Domain updates are no longer supported. Use DNS records instead."));
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||
@@ -349,7 +331,6 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
|
||||
UserDomain {
|
||||
name: domain.name,
|
||||
tld: domain.tld,
|
||||
ip: domain.ip,
|
||||
status: domain.status.unwrap_or_else(|| "pending".to_string()),
|
||||
denial_reason: domain.denial_reason,
|
||||
}
|
||||
@@ -464,13 +445,37 @@ pub(crate) async fn create_domain_record(ctx: &ServerContext, app_state: AppStat
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
|
||||
}
|
||||
|
||||
let valid_types = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV"];
|
||||
let valid_types = ["A", "AAAA", "CNAME", "TXT", "NS"];
|
||||
if !valid_types.contains(&record_data.record_type.as_str()) {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type. Only A, AAAA, CNAME, TXT, and NS records are supported."));
|
||||
}
|
||||
|
||||
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
|
||||
let ttl = record_data.ttl.unwrap_or(3600);
|
||||
|
||||
match record_data.record_type.as_str() {
|
||||
"A" => {
|
||||
if !record_data.value.parse::<std::net::Ipv4Addr>().is_ok() {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv4 address for A record"));
|
||||
}
|
||||
},
|
||||
"AAAA" => {
|
||||
if !record_data.value.parse::<std::net::Ipv6Addr>().is_ok() {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid IPv6 address for AAAA record"));
|
||||
}
|
||||
},
|
||||
"CNAME" | "NS" => {
|
||||
if record_data.value.is_empty() || !record_data.value.contains('.') {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("CNAME and NS records must contain a valid domain name"));
|
||||
}
|
||||
},
|
||||
"TXT" => {
|
||||
// TXT records can contain any text
|
||||
},
|
||||
_ => {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
|
||||
}
|
||||
}
|
||||
|
||||
let record_id: (i32,) = sqlx::query_as(
|
||||
"INSERT INTO dns_records (domain_id, record_type, name, value, ttl, priority) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id"
|
||||
@@ -550,6 +555,212 @@ pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppStat
|
||||
Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully"))
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
|
||||
let resolution_request: DnsResolutionRequest = serde_json::from_slice(ctx.body())
|
||||
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
|
||||
|
||||
let full_domain = format!("{}.{}", resolution_request.name, resolution_request.tld);
|
||||
|
||||
// Try to resolve with enhanced subdomain and delegation support
|
||||
match resolve_dns_with_delegation(&full_domain, &app_state).await {
|
||||
Ok(response) => Ok(GurtResponse::ok().with_json_body(&response)?),
|
||||
Err(_) => Ok(GurtResponse::not_found().with_json_body(&Error {
|
||||
msg: "Domain not found",
|
||||
error: "Domain not found, not approved, or delegation failed".into(),
|
||||
})?),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_dns_with_delegation(query_name: &str, app_state: &AppState) -> Result<DnsResolutionResponse> {
|
||||
// Parse the query domain
|
||||
let parts: Vec<&str> = query_name.split('.').collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(GurtError::invalid_message("Invalid domain format"));
|
||||
}
|
||||
|
||||
let tld = parts.last().unwrap();
|
||||
|
||||
// Try to find exact match first
|
||||
if let Some(response) = try_exact_match(query_name, tld, app_state).await? {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
// Try to find delegation by checking parent domains
|
||||
if let Some(response) = try_delegation_match(query_name, tld, app_state).await? {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
Err(GurtError::invalid_message("No matching records or delegation found"))
|
||||
}
|
||||
|
||||
async fn try_exact_match(query_name: &str, tld: &str, app_state: &AppState) -> Result<Option<DnsResolutionResponse>> {
|
||||
let parts: Vec<&str> = query_name.split('.').collect();
|
||||
if parts.len() < 2 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// For a query like "api.blog.example.com", try different combinations
|
||||
for i in (1..parts.len()).rev() {
|
||||
let domain_name = parts[parts.len() - i - 1];
|
||||
let subdomain_parts = &parts[0..parts.len() - i - 1];
|
||||
let subdomain = if subdomain_parts.is_empty() {
|
||||
"@".to_string()
|
||||
} else {
|
||||
subdomain_parts.join(".")
|
||||
};
|
||||
|
||||
// Look for the domain in our database
|
||||
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
||||
)
|
||||
.bind(domain_name)
|
||||
.bind(tld)
|
||||
.fetch_optional(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
if let Some(domain) = domain {
|
||||
// Look for specific records for this subdomain
|
||||
let records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
|
||||
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND name = $2 ORDER BY created_at ASC"
|
||||
)
|
||||
.bind(domain.id.unwrap())
|
||||
.bind(&subdomain)
|
||||
.fetch_all(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
if !records.is_empty() {
|
||||
let response_records: Vec<ResponseDnsRecord> = records.into_iter().map(|record| {
|
||||
ResponseDnsRecord {
|
||||
id: record.id.unwrap(),
|
||||
record_type: record.record_type,
|
||||
name: record.name,
|
||||
value: record.value,
|
||||
ttl: record.ttl.unwrap_or(3600),
|
||||
priority: record.priority,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
return Ok(Some(DnsResolutionResponse {
|
||||
name: query_name.to_string(),
|
||||
tld: tld.to_string(),
|
||||
records: response_records,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn try_delegation_match(query_name: &str, tld: &str, app_state: &AppState) -> Result<Option<DnsResolutionResponse>> {
|
||||
let parts: Vec<&str> = query_name.split('.').collect();
|
||||
|
||||
// Try to find NS records for parent domains
|
||||
for i in (1..parts.len()).rev() {
|
||||
let domain_name = parts[parts.len() - i - 1];
|
||||
let subdomain_parts = &parts[0..parts.len() - i - 1];
|
||||
let subdomain = if subdomain_parts.is_empty() {
|
||||
"@".to_string()
|
||||
} else {
|
||||
subdomain_parts.join(".")
|
||||
};
|
||||
|
||||
// Look for the domain
|
||||
let domain: Option<Domain> = sqlx::query_as::<_, Domain>(
|
||||
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE name = $1 AND tld = $2 AND status = 'approved'"
|
||||
)
|
||||
.bind(domain_name)
|
||||
.bind(tld)
|
||||
.fetch_optional(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
if let Some(domain) = domain {
|
||||
// Look for NS records that match this subdomain or parent
|
||||
let ns_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
|
||||
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND record_type = 'NS' AND (name = $2 OR name = $3) ORDER BY created_at ASC"
|
||||
)
|
||||
.bind(domain.id.unwrap())
|
||||
.bind(&subdomain)
|
||||
.bind("@")
|
||||
.fetch_all(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
if !ns_records.is_empty() {
|
||||
// Also look for glue records (A/AAAA records for the NS hosts)
|
||||
let mut all_records = ns_records;
|
||||
|
||||
// Get glue records for NS entries that point to subdomains of this zone
|
||||
for ns_record in &all_records.clone() {
|
||||
let ns_host = &ns_record.value;
|
||||
if ns_host.ends_with(&format!(".{}.{}", domain_name, tld)) ||
|
||||
ns_host == &format!("{}.{}", domain_name, tld) {
|
||||
|
||||
let glue_records: Vec<DnsRecord> = sqlx::query_as::<_, DnsRecord>(
|
||||
"SELECT id, domain_id, record_type, name, value, ttl, priority, created_at FROM dns_records WHERE domain_id = $1 AND (record_type = 'A' OR record_type = 'AAAA') AND value = $2"
|
||||
)
|
||||
.bind(domain.id.unwrap())
|
||||
.bind(ns_host)
|
||||
.fetch_all(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
all_records.extend(glue_records);
|
||||
}
|
||||
}
|
||||
|
||||
let response_records: Vec<ResponseDnsRecord> = all_records.into_iter().map(|record| {
|
||||
ResponseDnsRecord {
|
||||
id: record.id.unwrap(),
|
||||
record_type: record.record_type,
|
||||
name: record.name,
|
||||
value: record.value,
|
||||
ttl: record.ttl.unwrap_or(3600),
|
||||
priority: record.priority,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
return Ok(Some(DnsResolutionResponse {
|
||||
name: query_name.to_string(),
|
||||
tld: tld.to_string(),
|
||||
records: response_records,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_full_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FullDomainRequest {
|
||||
domain: String,
|
||||
record_type: Option<String>,
|
||||
}
|
||||
|
||||
let request: FullDomainRequest = serde_json::from_slice(ctx.body())
|
||||
.map_err(|_| GurtError::invalid_message("Invalid JSON"))?;
|
||||
|
||||
// Try to resolve with enhanced subdomain and delegation support
|
||||
match resolve_dns_with_delegation(&request.domain, &app_state).await {
|
||||
Ok(mut response) => {
|
||||
// Filter by record type if specified
|
||||
if let Some(record_type) = request.record_type {
|
||||
response.records.retain(|r| r.record_type == record_type);
|
||||
}
|
||||
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||
}
|
||||
Err(_) => Ok(GurtResponse::not_found().with_json_body(&Error {
|
||||
msg: "Domain not found",
|
||||
error: "Domain not found, not approved, or delegation failed".into(),
|
||||
})?),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Error {
|
||||
msg: &'static str,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
mod config;
|
||||
mod gurt_server;
|
||||
mod secret;
|
||||
mod auth;
|
||||
mod discord_bot;
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||
|
||||
pub fn generate(size: usize) -> String {
|
||||
const ALPHABET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let alphabet_len = ALPHABET.len();
|
||||
let mask = alphabet_len.next_power_of_two() - 1;
|
||||
let step = 8 * size / 5;
|
||||
|
||||
let mut id = String::with_capacity(size);
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
||||
while id.len() < size {
|
||||
let bytes: Vec<u8> = (0..step).map(|_| rng.gen::<u8>()).collect::<Vec<u8>>();
|
||||
|
||||
id.extend(
|
||||
bytes
|
||||
.iter()
|
||||
.map(|&byte| (byte as usize) & mask)
|
||||
.filter_map(|index| ALPHABET.get(index).copied())
|
||||
.take(size - id.len())
|
||||
.map(char::from),
|
||||
);
|
||||
}
|
||||
|
||||
id
|
||||
}
|
||||
Reference in New Issue
Block a user