diff --git a/dns/frontend/dashboard.lua b/dns/frontend/dashboard.lua
index 92734ea..43093e3 100644
--- a/dns/frontend/dashboard.lua
+++ b/dns/frontend/dashboard.lua
@@ -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
diff --git a/dns/frontend/domain.html b/dns/frontend/domain.html
new file mode 100644
index 0000000..5a422c7
--- /dev/null
+++ b/dns/frontend/domain.html
@@ -0,0 +1,137 @@
+
+ Domain Management
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Loading...
+
Status: Loading...
+
+
+
+
+
+
+
+
+
+
Records
+
+
Loading DNS records...
+
+
+
+
+
New Record
+
+
Type:
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dns/frontend/domain.lua b/dns/frontend/domain.lua
new file mode 100644
index 0000000..3700a76
--- /dev/null
+++ b/dns/frontend/domain.lua
@@ -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()
diff --git a/dns/src/gurt_server.rs b/dns/src/gurt_server.rs
index 9031e4b..ae1635c 100644
--- a/dns/src/gurt_server.rs
+++ b/dns/src/gurt_server.rs
@@ -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 });
diff --git a/dns/src/gurt_server/models.rs b/dns/src/gurt_server/models.rs
index 43c6d61..09d89a9 100644
--- a/dns/src/gurt_server/models.rs
+++ b/dns/src/gurt_server/models.rs
@@ -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,
+ pub(crate) ttl: i32,
pub(crate) priority: Option,
}
@@ -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,
+ pub(crate) value: String,
+ #[serde(deserialize_with = "deserialize_ttl")]
+ pub(crate) ttl: Option,
+ pub(crate) priority: Option,
+}
+
+fn deserialize_ttl<'de, D>(deserializer: D) -> Result