DNS record management, CSS grid, Regex, location.query
This commit is contained in:
@@ -44,7 +44,7 @@ local function renderDomains()
|
||||
|
||||
for i, domain in ipairs(domains) do
|
||||
local domainItem = gurt.create('div', {
|
||||
style = 'domain-item'
|
||||
style = 'domain-item cursor-pointer hover:bg-[#4b5563]'
|
||||
})
|
||||
|
||||
local domainInfo = gurt.create('div', { style = 'w-full' })
|
||||
@@ -54,51 +54,20 @@ local function renderDomains()
|
||||
style = 'font-bold text-lg'
|
||||
})
|
||||
|
||||
local domainIP = gurt.create('div', {
|
||||
text = 'IP: ' .. domain.ip,
|
||||
style = 'text-[#6b7280]'
|
||||
})
|
||||
|
||||
local domainStatus = gurt.create('div', {
|
||||
text = 'Status: ' .. (domain.status or 'Unknown'),
|
||||
style = 'text-[#6b7280]'
|
||||
})
|
||||
|
||||
domainInfo:append(domainName)
|
||||
domainInfo:append(domainIP)
|
||||
domainInfo:append(domainStatus)
|
||||
|
||||
local actions = gurt.create('div', {
|
||||
style = 'flex gap-2'
|
||||
})
|
||||
|
||||
-- Update IP button
|
||||
local updateBtn = gurt.create('button', {
|
||||
text = 'Update IP',
|
||||
style = 'secondary-btn'
|
||||
})
|
||||
|
||||
updateBtn:on('click', function()
|
||||
local newIP = prompt('Enter new IP address for ' .. domain.name .. '.' .. domain.tld .. ':')
|
||||
if newIP and newIP ~= '' then
|
||||
updateDomainIP(domain.name, domain.tld, newIP)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Delete button
|
||||
local deleteBtn = gurt.create('button', { text = 'Delete', style = 'danger-btn' })
|
||||
|
||||
deleteBtn:on('click', function()
|
||||
if confirm('Are you sure you want to delete ' .. domain.name .. '.' .. domain.tld .. '?') then
|
||||
deleteDomain(domain.name, domain.tld)
|
||||
end
|
||||
end)
|
||||
|
||||
actions:append(updateBtn)
|
||||
actions:append(deleteBtn)
|
||||
|
||||
domainItem:append(domainInfo)
|
||||
domainItem:append(actions)
|
||||
|
||||
domainItem:on('click', function()
|
||||
gurt.location.goto('/domain.html?name=' .. domain.name .. '.' .. domain.tld)
|
||||
end)
|
||||
|
||||
domainsList:append(domainItem)
|
||||
end
|
||||
end
|
||||
|
||||
137
dns/frontend/domain.html
Normal file
137
dns/frontend/domain.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<head>
|
||||
<title>Domain Management</title>
|
||||
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||
<meta name="theme-color" content="#0891b2">
|
||||
<meta name="description" content="Manage DNS records for your domain">
|
||||
|
||||
<style>
|
||||
body {
|
||||
bg-[#171616] font-sans text-white
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-[#ef4444] text-3xl font-bold text-center
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-[#dc2626] text-xl font-semibold
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-[#fca5a5] text-lg font-medium
|
||||
}
|
||||
|
||||
.container {
|
||||
bg-[#262626] p-6 rounded-lg shadow-lg max-w-6xl
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||
}
|
||||
|
||||
.success-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#525252] text-white
|
||||
}
|
||||
|
||||
.form-group {
|
||||
flex flex-col gap-2 mb-4 w-full
|
||||
}
|
||||
|
||||
.form-input {
|
||||
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500
|
||||
}
|
||||
|
||||
.form-select {
|
||||
w-full p-3 border border-gray-600 rounded-md bg-[#374151] text-white active:border-red-500 active:text-white
|
||||
}
|
||||
|
||||
.card {
|
||||
bg-[#262626] p-6 rounded-lg shadow border border-gray-700
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
||||
}
|
||||
|
||||
.record-item {
|
||||
bg-[#374151] p-4 rounded-lg border border-gray-700 mb-2 flex justify-between items-center
|
||||
}
|
||||
|
||||
.error-text {
|
||||
text-[#fca5a5] text-sm
|
||||
}
|
||||
|
||||
.record-table {
|
||||
w-full border
|
||||
}
|
||||
|
||||
.record-table th {
|
||||
p-3 text-left border-b border-gray-600 text-[#dc2626]
|
||||
}
|
||||
|
||||
.record-table td {
|
||||
p-3 border-b border-gray-700
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="domain.lua" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="container mt-6">
|
||||
<div style="stats-card mb-6">
|
||||
<div style="flex justify-between items-center w-full">
|
||||
<div>
|
||||
<h1 id="domain-title">Loading...</h1>
|
||||
<p id="domain-status" style="text-[#6b7280]">Status: Loading...</p>
|
||||
</div>
|
||||
<div style="flex gap-2">
|
||||
<button id="back-btn" style="secondary-btn">Back</button>
|
||||
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="card mb-6">
|
||||
<h2 style="mb-4">Records</h2>
|
||||
<div id="records-list">
|
||||
<p id="records-loading">Loading DNS records...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="card">
|
||||
<h2 style="mb-4">New Record</h2>
|
||||
<div style="form-group">
|
||||
<p>Type:</p>
|
||||
<select id="record-type" style="form-select">
|
||||
<option value="A">A</option>
|
||||
<option value="AAAA">AAAA</option>
|
||||
<option value="CNAME">CNAME</option>
|
||||
<option value="TXT">TXT</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>Name:</p>
|
||||
<input id="record-name" type="text" style="form-input" placeholder="@, www, *" pattern="^(?:\*|@|((?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*))$" />
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>Value:</p>
|
||||
<input id="record-value" type="text" style="form-input" placeholder="192.168.1.1, example.com" />
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>TTL:</p>
|
||||
<input id="record-ttl" type="text" style="form-input" placeholder="3600" pattern="^[0-9]+$" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="record-error" style="error-text hidden mb-2"></div>
|
||||
<button id="add-record-btn" style="success-btn">Add Record</button>
|
||||
</div>
|
||||
</body>
|
||||
300
dns/frontend/domain.lua
Normal file
300
dns/frontend/domain.lua
Normal file
@@ -0,0 +1,300 @@
|
||||
local user = nil
|
||||
local domain = nil
|
||||
local records = {}
|
||||
local authToken = nil
|
||||
local domainName = nil
|
||||
|
||||
local domainTitle = gurt.select('#domain-title')
|
||||
local domainStatus = gurt.select('#domain-status')
|
||||
local recordsList = gurt.select('#records-list')
|
||||
local loadingElement = gurt.select('#records-loading')
|
||||
|
||||
local function showError(elementId, message)
|
||||
local element = gurt.select('#' .. elementId)
|
||||
|
||||
element.text = message
|
||||
element.classList:remove('hidden')
|
||||
end
|
||||
|
||||
local function hideError(elementId)
|
||||
local element = gurt.select('#' .. elementId)
|
||||
|
||||
element.classList:add('hidden')
|
||||
end
|
||||
|
||||
-- Forward declarations
|
||||
local loadRecords
|
||||
local renderRecords
|
||||
|
||||
local function deleteRecord(recordId)
|
||||
print('Deleting DNS record: ' .. recordId)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records/' .. recordId, {
|
||||
method = 'DELETE',
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
print('DNS record deleted successfully')
|
||||
|
||||
-- Remove the record from local records array
|
||||
for i = #records, 1, -1 do
|
||||
if records[i].id == recordId then
|
||||
table.remove(records, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Re-render the entire list from scratch
|
||||
renderRecords()
|
||||
else
|
||||
print('Failed to delete DNS record: ' .. response:text())
|
||||
end
|
||||
end
|
||||
|
||||
-- Actual implementation
|
||||
loadRecords = function()
|
||||
print('Loading DNS records for: ' .. domainName)
|
||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
records = response:json()
|
||||
print('Loaded ' .. #records .. ' DNS records')
|
||||
renderRecords()
|
||||
else
|
||||
print('Failed to load DNS records: ' .. response:text())
|
||||
records = {}
|
||||
renderRecords()
|
||||
end
|
||||
end
|
||||
|
||||
renderRecords = function(appendOnly)
|
||||
if loadingElement then
|
||||
loadingElement:remove()
|
||||
loadingElement = nil
|
||||
end
|
||||
|
||||
-- Clear everything if not appending
|
||||
if not appendOnly then
|
||||
local children = recordsList.children
|
||||
while #children > 0 do
|
||||
children[1]:remove()
|
||||
children = recordsList.children
|
||||
end
|
||||
end
|
||||
|
||||
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'
|
||||
})
|
||||
recordsList:append(emptyMessage)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create header only if not appending or if list is empty
|
||||
if not appendOnly or #recordsList.children == 0 then
|
||||
local header = gurt.create('div', { style = 'w-full flex justify-between gap-4 p-4 bg-gray-600 font-bold border-b rounded-xl' })
|
||||
|
||||
local typeHeader = gurt.create('div', { text = 'Type' })
|
||||
local nameHeader = gurt.create('div', { text = 'Name' })
|
||||
local valueHeader = gurt.create('div', { text = 'Value' })
|
||||
local ttlHeader = gurt.create('div', { text = 'TTL' })
|
||||
local actionsHeader = gurt.create('div', { text = 'Actions' })
|
||||
|
||||
header:append(typeHeader)
|
||||
header:append(nameHeader)
|
||||
header:append(valueHeader)
|
||||
header:append(ttlHeader)
|
||||
header:append(actionsHeader)
|
||||
recordsList:append(header)
|
||||
end
|
||||
|
||||
-- Create records list - when appending, only render the last record; otherwise render all
|
||||
local startIndex = appendOnly and #records or 1
|
||||
for i = startIndex, #records do
|
||||
local record = records[i]
|
||||
local row = gurt.create('div', { style = 'w-full flex justify-between gap-4 p-4 border-b border-gray-600 hover:bg-[rgba(244, 67, 54, 0.2)]' })
|
||||
|
||||
local typeCell = gurt.create('div', { text = record.type, style = 'font-bold' })
|
||||
local nameCell = gurt.create('div', { text = record.name or '@' })
|
||||
local valueCell = gurt.create('div', { text = record.value, style = 'font-mono text-sm break-all' })
|
||||
local ttlCell = gurt.create('div', { text = record.ttl or '3600' })
|
||||
|
||||
local actionsCell = gurt.create('div')
|
||||
local deleteBtn = gurt.create('button', {
|
||||
text = 'Delete',
|
||||
style = 'danger-btn text-xs px-2 py-1'
|
||||
})
|
||||
|
||||
deleteBtn:on('click', function()
|
||||
if deleteBtn.text == 'Delete' then
|
||||
deleteBtn.text = 'Confirm Delete'
|
||||
else
|
||||
deleteRecord(record.id)
|
||||
end
|
||||
end)
|
||||
|
||||
actionsCell:append(deleteBtn)
|
||||
|
||||
row:append(typeCell)
|
||||
row:append(nameCell)
|
||||
row:append(valueCell)
|
||||
row:append(ttlCell)
|
||||
row:append(actionsCell)
|
||||
recordsList:append(row)
|
||||
end
|
||||
end
|
||||
|
||||
local function getDomainNameFromURL()
|
||||
local nameParam = gurt.location.query.get('name')
|
||||
if nameParam then
|
||||
return nameParam:gsub('%%%.', '.')
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function updateDomainInfo()
|
||||
if domain then
|
||||
domainTitle.text = domain.name .. '.' .. domain.tld
|
||||
domainStatus.text = 'Status: ' .. (domain.status or 'Unknown')
|
||||
end
|
||||
end
|
||||
|
||||
local function loadDomain()
|
||||
print('Loading domain details for: ' .. domainName)
|
||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName, {
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
domain = response:json()
|
||||
print('Loaded domain details')
|
||||
updateDomainInfo()
|
||||
loadRecords()
|
||||
else
|
||||
print('Failed to load domain: ' .. response:text())
|
||||
--gurt.location.goto('/dashboard.html')
|
||||
end
|
||||
end
|
||||
|
||||
local function checkAuth()
|
||||
authToken = gurt.crumbs.get("auth_token")
|
||||
|
||||
if authToken then
|
||||
print('Found auth token, checking validity...')
|
||||
local response = fetch('gurt://localhost:8877/auth/me', {
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
user = response:json()
|
||||
print('Authentication successful for user: ' .. user.username)
|
||||
|
||||
domainName = getDomainNameFromURL()
|
||||
if domainName then
|
||||
loadDomain()
|
||||
else
|
||||
print('No domain name in URL, redirecting to dashboard')
|
||||
--gurt.location.goto('/dashboard.html')
|
||||
end
|
||||
else
|
||||
print('Token invalid, redirecting to login...')
|
||||
gurt.crumbs.delete('auth_token')
|
||||
--gurt.location.goto('../')
|
||||
end
|
||||
else
|
||||
print('No auth token found, redirecting to login...')
|
||||
--gurt.location.goto('../')
|
||||
end
|
||||
end
|
||||
|
||||
local function addRecord(type, name, value, ttl)
|
||||
hideError('record-error')
|
||||
print('Adding DNS record: ' .. type .. ' ' .. name .. ' ' .. value)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/domain/' .. domainName .. '/records', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({
|
||||
type = type,
|
||||
name = name,
|
||||
value = value,
|
||||
ttl = ttl
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
-- Render only the new record
|
||||
renderRecords(true)
|
||||
else
|
||||
-- Server didn't return record details, reload to get the actual data
|
||||
loadRecords()
|
||||
end
|
||||
else
|
||||
local error = response:text()
|
||||
showError('record-error', 'Failed to add record: ' .. error)
|
||||
print('Failed to add DNS record: ' .. error)
|
||||
end
|
||||
end
|
||||
|
||||
local function logout()
|
||||
gurt.crumbs.delete('auth_token')
|
||||
print('Logged out successfully')
|
||||
--gurt.location.goto("../")
|
||||
end
|
||||
|
||||
local function goBack()
|
||||
--gurt.location.goto("/dashboard.html")
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
gurt.select('#logout-btn'):on('click', logout)
|
||||
gurt.select('#back-btn'):on('click', goBack)
|
||||
|
||||
gurt.select('#add-record-btn'):on('click', function()
|
||||
local recordType = gurt.select('#record-type').value
|
||||
local recordName = gurt.select('#record-name').value
|
||||
local recordValue = gurt.select('#record-value').value
|
||||
local recordTTL = tonumber(gurt.select('#record-ttl').value) or 3600
|
||||
|
||||
if not recordValue or recordValue == '' then
|
||||
showError('record-error', 'Record value is required')
|
||||
return
|
||||
end
|
||||
|
||||
if not recordName or recordName == '' then
|
||||
recordName = '@'
|
||||
end
|
||||
|
||||
addRecord(recordType, recordName, recordValue, recordTTL)
|
||||
end)
|
||||
|
||||
-- Initialize
|
||||
print('Domain management page initialized')
|
||||
checkAuth()
|
||||
@@ -94,6 +94,7 @@ enum HandlerType {
|
||||
UpdateDomain,
|
||||
DeleteDomain,
|
||||
GetUserDomains,
|
||||
CreateDomainRecord,
|
||||
}
|
||||
|
||||
impl GurtHandler for AppHandler {
|
||||
@@ -118,7 +119,13 @@ impl GurtHandler for AppHandler {
|
||||
|
||||
let result = match handler_type {
|
||||
HandlerType::Index => routes::index(app_state).await,
|
||||
HandlerType::GetDomain => routes::get_domain(&ctx, app_state).await,
|
||||
HandlerType::GetDomain => {
|
||||
if ctx.path().contains("/records") {
|
||||
handle_authenticated!(ctx, app_state, routes::get_domain_records)
|
||||
} else {
|
||||
handle_authenticated!(ctx, app_state, routes::get_domain)
|
||||
}
|
||||
},
|
||||
HandlerType::GetDomains => routes::get_domains(&ctx, app_state).await,
|
||||
HandlerType::GetTlds => routes::get_tlds(app_state).await,
|
||||
HandlerType::CheckDomain => routes::check_domain(&ctx, app_state).await,
|
||||
@@ -130,6 +137,13 @@ impl GurtHandler for AppHandler {
|
||||
HandlerType::CreateDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::create_domain_invite),
|
||||
HandlerType::RedeemDomainInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_domain_invite),
|
||||
HandlerType::GetUserDomains => handle_authenticated!(ctx, app_state, routes::get_user_domains),
|
||||
HandlerType::CreateDomainRecord => {
|
||||
if ctx.path().contains("/records") {
|
||||
handle_authenticated!(ctx, app_state, routes::create_domain_record)
|
||||
} else {
|
||||
Ok(GurtResponse::new(GurtStatusCode::MethodNotAllowed).with_string_body("Method not allowed"))
|
||||
}
|
||||
},
|
||||
HandlerType::CreateDomain => {
|
||||
// Check rate limit first
|
||||
if let Some(ref rate_limit_state) = rate_limit_state {
|
||||
@@ -142,7 +156,13 @@ impl GurtHandler for AppHandler {
|
||||
handle_authenticated!(ctx, app_state, routes::create_domain)
|
||||
},
|
||||
HandlerType::UpdateDomain => handle_authenticated!(ctx, app_state, routes::update_domain),
|
||||
HandlerType::DeleteDomain => handle_authenticated!(ctx, app_state, routes::delete_domain),
|
||||
HandlerType::DeleteDomain => {
|
||||
if ctx.path().contains("/records/") {
|
||||
handle_authenticated!(ctx, app_state, routes::delete_domain_record)
|
||||
} else {
|
||||
handle_authenticated!(ctx, app_state, routes::delete_domain)
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let duration = start_time.elapsed();
|
||||
@@ -196,7 +216,6 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
||||
|
||||
server = server
|
||||
.route(Route::get("/"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::Index })
|
||||
.route(Route::get("/domain/*"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomain })
|
||||
.route(Route::get("/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetDomains })
|
||||
.route(Route::get("/tlds"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetTlds })
|
||||
.route(Route::get("/check"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CheckDomain })
|
||||
@@ -209,6 +228,8 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
||||
.route(Route::post("/auth/redeem-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemDomainInvite })
|
||||
.route(Route::get("/auth/domains"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::GetUserDomains })
|
||||
.route(Route::post("/domain"), AppHandler { app_state: app_state.clone(), rate_limit_state: Some(rate_limit_state), handler_type: HandlerType::CreateDomain })
|
||||
.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 });
|
||||
|
||||
|
||||
@@ -77,10 +77,12 @@ pub(crate) struct ResponseDomain {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct ResponseDnsRecord {
|
||||
pub(crate) id: i32,
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) record_type: String,
|
||||
pub(crate) name: String,
|
||||
pub(crate) value: String,
|
||||
pub(crate) ttl: Option<i32>,
|
||||
pub(crate) ttl: i32,
|
||||
pub(crate) priority: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -117,3 +119,43 @@ pub(crate) struct UserDomainResponse {
|
||||
pub(crate) page: u32,
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct CreateDnsRecord {
|
||||
#[serde(rename = "type")]
|
||||
pub(crate) record_type: String,
|
||||
pub(crate) name: Option<String>,
|
||||
pub(crate) value: String,
|
||||
#[serde(deserialize_with = "deserialize_ttl")]
|
||||
pub(crate) ttl: Option<i32>,
|
||||
pub(crate) priority: Option<i32>,
|
||||
}
|
||||
|
||||
fn deserialize_ttl<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
use serde::Deserialize;
|
||||
|
||||
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
|
||||
match value {
|
||||
Some(serde_json::Value::Number(n)) => {
|
||||
if let Some(f) = n.as_f64() {
|
||||
Ok(Some(f as i32))
|
||||
} else if let Some(i) = n.as_i64() {
|
||||
Ok(Some(i as i32))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
Some(_) => Ok(None),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub(crate) struct DomainDetail {
|
||||
pub(crate) name: String,
|
||||
pub(crate) tld: String,
|
||||
pub(crate) status: Option<String>,
|
||||
}
|
||||
|
||||
@@ -103,31 +103,38 @@ pub(crate) async fn create_domain(ctx: &ServerContext, app_state: AppState, clai
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_domain(ctx: &ServerContext, app_state: AppState) -> Result<GurtResponse> {
|
||||
pub(crate) async fn get_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}"));
|
||||
if path_parts.len() < 3 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}"));
|
||||
}
|
||||
|
||||
let name = path_parts[2];
|
||||
let tld = path_parts[3];
|
||||
let domain_name = path_parts[2];
|
||||
|
||||
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||
if domain_parts.len() < 2 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||
}
|
||||
|
||||
let name = domain_parts[0];
|
||||
let tld = domain_parts[1];
|
||||
|
||||
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'"
|
||||
"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"))?;
|
||||
|
||||
match domain {
|
||||
Some(domain) => {
|
||||
let response_domain = ResponseDomain {
|
||||
let response_domain = DomainDetail {
|
||||
name: domain.name,
|
||||
tld: domain.tld,
|
||||
ip: domain.ip,
|
||||
records: None, // TODO: Implement DNS records
|
||||
status: domain.status,
|
||||
};
|
||||
Ok(GurtResponse::ok().with_json_body(&response_domain)?)
|
||||
}
|
||||
@@ -357,6 +364,192 @@ pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, c
|
||||
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_domain_records(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/{domainName}/records"));
|
||||
}
|
||||
|
||||
let domain_name = path_parts[2];
|
||||
|
||||
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||
if domain_parts.len() < 2 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||
}
|
||||
|
||||
let name = domain_parts[0];
|
||||
let tld = domain_parts[1];
|
||||
|
||||
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"))
|
||||
};
|
||||
|
||||
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 ORDER BY created_at ASC"
|
||||
)
|
||||
.bind(domain.id.unwrap())
|
||||
.fetch_all(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
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();
|
||||
|
||||
Ok(GurtResponse::ok().with_json_body(&response_records)?)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_domain_record(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/{domainName}/records"));
|
||||
}
|
||||
|
||||
let domain_name = path_parts[2];
|
||||
|
||||
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||
if domain_parts.len() < 2 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||
}
|
||||
|
||||
let name = domain_parts[0];
|
||||
let tld = domain_parts[1];
|
||||
|
||||
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"))
|
||||
};
|
||||
|
||||
let record_data: CreateDnsRecord = {
|
||||
let body_bytes = ctx.body();
|
||||
let body_str = std::str::from_utf8(body_bytes).unwrap_or("<invalid utf8>");
|
||||
log::info!("Received JSON body: {}", body_str);
|
||||
|
||||
serde_json::from_slice(body_bytes)
|
||||
.map_err(|e| {
|
||||
log::error!("JSON parsing error: {} for body: {}", e, body_str);
|
||||
GurtError::invalid_message("Invalid JSON")
|
||||
})?
|
||||
};
|
||||
|
||||
if record_data.record_type.is_empty() {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Record type is required"));
|
||||
}
|
||||
|
||||
let valid_types = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV"];
|
||||
if !valid_types.contains(&record_data.record_type.as_str()) {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid record type"));
|
||||
}
|
||||
|
||||
let record_name = record_data.name.unwrap_or_else(|| "@".to_string());
|
||||
let ttl = record_data.ttl.unwrap_or(3600);
|
||||
|
||||
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"
|
||||
)
|
||||
.bind(domain.id.unwrap())
|
||||
.bind(&record_data.record_type)
|
||||
.bind(&record_name)
|
||||
.bind(&record_data.value)
|
||||
.bind(ttl)
|
||||
.bind(record_data.priority)
|
||||
.fetch_one(&app_state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to create DNS record: {}", e);
|
||||
GurtError::invalid_message("Failed to create DNS record")
|
||||
})?;
|
||||
|
||||
let response_record = ResponseDnsRecord {
|
||||
id: record_id.0,
|
||||
record_type: record_data.record_type,
|
||||
name: record_name,
|
||||
value: record_data.value,
|
||||
ttl,
|
||||
priority: record_data.priority,
|
||||
};
|
||||
|
||||
Ok(GurtResponse::ok().with_json_body(&response_record)?)
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_domain_record(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||
let path_parts: Vec<&str> = ctx.path().split('/').collect();
|
||||
if path_parts.len() < 5 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid path format. Expected /domain/{domainName}/records/{recordId}"));
|
||||
}
|
||||
|
||||
let domain_name = path_parts[2];
|
||||
let record_id_str = path_parts[4];
|
||||
|
||||
let record_id: i32 = record_id_str.parse()
|
||||
.map_err(|_| GurtError::invalid_message("Invalid record ID"))?;
|
||||
|
||||
let domain_parts: Vec<&str> = domain_name.split('.').collect();
|
||||
if domain_parts.len() < 2 {
|
||||
return Ok(GurtResponse::bad_request().with_string_body("Invalid domain format. Expected name.tld"));
|
||||
}
|
||||
|
||||
let name = domain_parts[0];
|
||||
let tld = domain_parts[1];
|
||||
|
||||
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"))
|
||||
};
|
||||
|
||||
let rows_affected = sqlx::query("DELETE FROM dns_records WHERE id = $1 AND domain_id = $2")
|
||||
.bind(record_id)
|
||||
.bind(domain.id.unwrap())
|
||||
.execute(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?
|
||||
.rows_affected();
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Ok(GurtResponse::not_found().with_string_body("DNS record not found"));
|
||||
}
|
||||
|
||||
Ok(GurtResponse::ok().with_string_body("DNS record deleted successfully"))
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Error {
|
||||
msg: &'static str,
|
||||
|
||||
Reference in New Issue
Block a user