diff --git a/dns/frontend/dashboard.html b/dns/frontend/dashboard.html
index 2e75510..044b754 100644
--- a/dns/frontend/dashboard.html
+++ b/dns/frontend/dashboard.html
@@ -1,10 +1,114 @@
- Login
+ Domain Dashboard
-
-
+
+
+
+
+
+
- todo
+ 🌐 Domain Management Dashboard
+
+
+
+
+
+
Register New Domain
+
+
+
+
+
+
+
+ Loading TLDs...
+
+
+
+
+
+
+
+
+
+
+
+
Invite System
+
Create invite codes to share with friends, or redeem codes to get more domain registrations.
+
+
+
+
Create Invite
+
+
+
+
Redeem Invite
+
+
+
+
+
+
+
+
+
+
+
My Domains
+
+ Loading domains...
+
+
+
+
+
+
+
+
+
Invite Code Generated
+
Share this code with friends to give them 3 additional domain registrations:
+
+ Loading...
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dns/frontend/dashboard.lua b/dns/frontend/dashboard.lua
new file mode 100644
index 0000000..197ff38
--- /dev/null
+++ b/dns/frontend/dashboard.lua
@@ -0,0 +1,364 @@
+local user = nil
+local domains = {}
+local tlds = {}
+local authToken = nil
+
+local userInfo = gurt.select('#user-info')
+local domainsList = gurt.select('#domains-list')
+local logArea = gurt.select('#log-area')
+local inviteModal = gurt.select('#invite-modal')
+local tldSelector = gurt.select('#tld-selector')
+
+local logMessages = {}
+
+local function addLog(message)
+ table.insert(logMessages, Time.format(Time.now(), '%H:%M:%S') .. ' - ' .. message)
+ if #logMessages > 50 then
+ table.remove(logMessages, 1)
+ end
+ logArea.text = table.concat(logMessages, '\n')
+end
+
+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
+
+local function showModal(modalId)
+ local modal = gurt.select('#' .. modalId)
+
+ modal.classList:remove('hidden')
+end
+
+local function hideModal(modalId)
+ local modal = gurt.select('#' .. modalId)
+
+ modal.classList:add('hidden')
+end
+
+local function makeRequest(url, options)
+ options = options or {}
+ if authToken then
+ options.headers = options.headers or {}
+ options.headers.Authorization = 'Bearer ' .. authToken
+ end
+ return fetch(url, options)
+end
+
+local function checkAuth()
+ authToken = gurt.crumbs.get("auth_token")
+
+ if authToken then
+ addLog('Found auth token, checking validity...')
+ local response = makeRequest('gurt://localhost:4878/auth/me')
+ print(table.tostring(response))
+ if response:ok() then
+ user = response:json()
+ addLog('Authentication successful for user: ' .. user.username)
+ updateUserInfo()
+ loadDomains()
+ loadTLDs()
+ else
+ addLog('Token invalid, redirecting to login...')
+ --gurt.crumbs.delete('auth_token')
+ --gurt.location.goto('../')
+ end
+ else
+ addLog('No auth token found, redirecting to login...')
+ gurt.location.goto('../')
+ end
+end
+
+local function logout()
+ gurt.crumbs.delete('auth_token')
+ addLog('Logged out successfully')
+ gurt.location.goto("../")
+end
+
+local function loadDomains()
+ addLog('Loading domains...')
+ local response = makeRequest('gurt://localhost:4878/domains?page=1&size=100')
+
+ if response:ok() then
+ local data = response:json()
+ domains = data.domains or {}
+ addLog('Loaded ' .. #domains .. ' domains')
+ renderDomains()
+ else
+ addLog('Failed to load domains: ' .. response:text())
+ end
+end
+
+local function loadTLDs()
+ addLog('Loading available TLDs...')
+ local response = fetch('gurt://localhost:4878/tlds')
+
+ if response:ok() then
+ tlds = response:json()
+ addLog('Loaded ' .. #tlds .. ' TLDs')
+ renderTLDSelector()
+ else
+ addLog('Failed to load TLDs: ' .. response:text())
+ end
+end
+
+local function submitDomain(name, tld, ip)
+ hideError('domain-error')
+ addLog('Submitting domain: ' .. name .. '.' .. tld)
+
+ local response = makeRequest('gurt://localhost:4878/domain', {
+ method = 'POST',
+ headers = { ['Content-Type'] = 'application/json' },
+ body = JSON.stringify({ name = name, tld = tld, ip = ip })
+ })
+
+ if response:ok() then
+ local data = response:json()
+ addLog('Domain submitted successfully: ' .. data.domain)
+
+ -- Update user registrations remaining
+ user.registrations_remaining = user.registrations_remaining - 1
+ updateUserInfo()
+
+ -- Clear form
+ gurt.select('#domain-name').text = ''
+ gurt.select('#domain-ip').text = ''
+
+ -- Refresh domains list
+ loadDomains()
+ else
+ local error = response:text()
+ showError('domain-error', 'Domain submission failed: ' .. error)
+ addLog('Domain submission failed: ' .. error)
+ end
+end
+
+local function createInvite()
+ addLog('Creating invite code...')
+ local response = makeRequest('gurt://localhost:4878/auth/invite', { method = 'POST' })
+
+ if response:ok() then
+ local data = response:json()
+ local inviteCode = data.invite_code
+ gurt.select('#invite-code-display').text = inviteCode
+ addLog('Invite code created: ' .. inviteCode)
+ showModal('invite-modal')
+ else
+ addLog('Failed to create invite: ' .. response:text())
+ end
+end
+
+local function redeemInvite(code)
+ hideError('redeem-error')
+ addLog('Redeeming invite code: ' .. code)
+
+ local response = makeRequest('gurt://localhost:4878/auth/redeem-invite', {
+ method = 'POST',
+ headers = { ['Content-Type'] = 'application/json' },
+ body = JSON.stringify({ invite_code = code })
+ })
+
+ if response:ok() then
+ local data = response:json()
+ addLog('Invite redeemed: +' .. data.registrations_added .. ' registrations')
+
+ -- Update user info
+ user.registrations_remaining = user.registrations_remaining + data.registrations_added
+ updateUserInfo()
+
+ -- Clear form
+ gurt.select('#invite-code-input').text = ''
+ else
+ local error = response:text()
+ showError('redeem-error', 'Failed to redeem invite: ' .. error)
+ addLog('Failed to redeem invite: ' .. error)
+ end
+end
+
+-- UI rendering functions
+local function updateUserInfo()
+ if user then
+ userInfo.text = 'Welcome, ' .. user.username .. ' | Registrations remaining: ' .. user.registrations_remaining
+ end
+end
+
+local function renderTLDSelector()
+ tldSelector.text = ''
+ for i, tld in ipairs(tlds) do
+ local option = gurt.create('div', {
+ text = '.' .. tld,
+ style = 'tld-option',
+ ['data-tld'] = tld
+ })
+
+ option:on('click', function()
+ -- Clear previous selection
+ local options = gurt.selectAll('.tld-option')
+ for j = 1, #options do
+ options[j].classList:remove('tld-selected')
+ end
+
+ -- Select this option
+ option.classList:add('tld-selected')
+ end)
+
+ tldSelector:append(option)
+ end
+end
+
+local function renderDomains()
+ domainsList.text = ''
+
+ if #domains == 0 then
+ local emptyMessage = gurt.create('div', {
+ text = 'No domains registered yet. Submit your first domain below!',
+ style = 'text-center text-[#6b7280] py-8'
+ })
+ domainsList:append(emptyMessage)
+ return
+ end
+
+ for i, domain in ipairs(domains) do
+ local domainItem = gurt.create('div', {
+ style = 'domain-item'
+ })
+
+ local domainInfo = gurt.create('div', {})
+
+ local domainName = gurt.create('div', {
+ text = domain.name .. '.' .. domain.tld,
+ style = 'font-bold text-lg'
+ })
+
+ local domainIP = gurt.create('div', {
+ text = 'IP: ' .. domain.ip,
+ style = 'text-[#6b7280]'
+ })
+
+ domainInfo:append(domainName)
+ domainInfo:append(domainIP)
+
+ 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)
+ domainsList:append(domainItem)
+ end
+end
+
+local function updateDomainIP(name, tld, ip)
+ addLog('Updating IP for ' .. name .. '.' .. tld .. ' to ' .. ip)
+
+ local response = makeRequest('gurt://localhost:4878/domain/' .. name .. '/' .. tld, {
+ method = 'PUT',
+ headers = { ['Content-Type'] = 'application/json' },
+ body = JSON.stringify({ ip = ip })
+ })
+
+ if response:ok() then
+ addLog('Domain IP updated successfully')
+ loadDomains()
+ else
+ addLog('Failed to update domain IP: ' .. response:text())
+ end
+end
+
+local function deleteDomain(name, tld)
+ addLog('Deleting domain: ' .. name .. '.' .. tld)
+
+ local response = makeRequest('gurt://localhost:4878/domain/' .. name .. '/' .. tld, {
+ method = 'DELETE'
+ })
+
+ if response:ok() then
+ addLog('Domain deleted successfully')
+ loadDomains()
+ else
+ addLog('Failed to delete domain: ' .. response:text())
+ end
+end
+
+-- Event handlers
+gurt.select('#logout-btn'):on('click', logout)
+
+gurt.select('#submit-domain-btn'):on('click', function()
+ local name = gurt.select('#domain-name').text
+ local ip = gurt.select('#domain-ip').text
+ local selectedTLD = gurt.select('.tld-selected')
+
+ if not name or name == '' then
+ showError('domain-error', 'Domain name is required')
+ return
+ end
+
+ if not ip or ip == '' then
+ showError('domain-error', 'IP address is required')
+ return
+ end
+
+ if not selectedTLD then
+ showError('domain-error', 'Please select a TLD')
+ return
+ end
+
+ local tld = selectedTLD:getAttribute('data-tld')
+ submitDomain(name, tld, ip)
+end)
+
+gurt.select('#create-invite-btn'):on('click', createInvite)
+
+gurt.select('#redeem-invite-btn'):on('click', function()
+ local code = gurt.select('#invite-code-input').text
+ if code and code ~= '' then
+ redeemInvite(code)
+ end
+end)
+
+gurt.select('#close-invite-modal'):on('click', function()
+ hideModal('invite-modal')
+end)
+
+gurt.select('#copy-invite-code'):on('click', function()
+ local inviteCode = gurt.select('#invite-code-display').text
+ Clipboard.write(inviteCode)
+ addLog('Invite code copied to clipboard')
+end)
+
+-- Initialize
+addLog('Dashboard initialized')
+checkAuth()
\ No newline at end of file
diff --git a/dns/frontend/script.lua b/dns/frontend/script.lua
index b9f4bdd..e799fd9 100644
--- a/dns/frontend/script.lua
+++ b/dns/frontend/script.lua
@@ -18,7 +18,7 @@ submitBtn:on('submit', function(event)
password = password
})
print(request_body)
- local url = 'http://localhost:8080/auth/login'
+ local url = 'gurt://localhost:8080/auth/login'
local headers = {
['Content-Type'] = 'application/json'
}
@@ -39,9 +39,13 @@ submitBtn:on('submit', function(event)
local jsonData = response:json()
if jsonData then
addLog('Logged in as user: ' .. jsonData.user.username)
- addLog('Token: ' .. jsonData.token:sub(1, 20) .. '...')
- -- TODO: store as cookie
+ gurt.crumbs.set({
+ name = "auth_token",
+ value = jsonData.token,
+ lifespan = 604800
+ })
+
gurt.location.goto("/dashboard.html")
end
else
diff --git a/flumi/Scripts/B9/CSSParser.gd b/flumi/Scripts/B9/CSSParser.gd
index dfb529f..35b5c91 100644
--- a/flumi/Scripts/B9/CSSParser.gd
+++ b/flumi/Scripts/B9/CSSParser.gd
@@ -913,6 +913,10 @@ static func parse_utility_class_internal(rule: CSSRule, utility_name: String) ->
rule.properties[utility_name] = "200ms"
return
+ if utility_name == "hidden":
+ rule.properties["display"] = "none"
+ return
+
# Handle more utility classes as needed
# Add more cases here for other utilities
diff --git a/flumi/Scripts/B9/HTMLParser.gd b/flumi/Scripts/B9/HTMLParser.gd
index 71bdb06..d4772cc 100644
--- a/flumi/Scripts/B9/HTMLParser.gd
+++ b/flumi/Scripts/B9/HTMLParser.gd
@@ -112,15 +112,15 @@ func handle_style_element(style_element: HTMLElement) -> void:
parse_result.external_css.append(src)
return
- # Handle inline CSS - we'll get the text content when parsing is complete
- # For now, create a parser that will be populated later
+ # Handle inline CSS
if not parse_result.css_parser:
parse_result.css_parser = CSSParser.new()
parse_result.css_parser.init()
func process_styles() -> void:
if not parse_result.css_parser:
- return
+ parse_result.css_parser = CSSParser.new()
+ parse_result.css_parser.init()
var css_content = Constants.DEFAULT_CSS
var style_elements = find_all("style")
diff --git a/flumi/Scripts/StyleManager.gd b/flumi/Scripts/StyleManager.gd
index ba8a069..877dc3f 100644
--- a/flumi/Scripts/StyleManager.gd
+++ b/flumi/Scripts/StyleManager.gd
@@ -109,6 +109,12 @@ static func apply_element_styles(node: Control, element: HTMLParser.HTMLElement,
if styles.has("opacity"):
node.modulate.a = styles["opacity"]
+ if styles.has("display"):
+ if styles["display"] == "none":
+ node.visible = false
+ else:
+ node.visible = true
+
# Apply cursor
if styles.has("cursor"):
var cursor_shape = get_cursor_shape_from_type(styles["cursor"])
diff --git a/flumi/Scripts/Utils/Lua/Network.gd b/flumi/Scripts/Utils/Lua/Network.gd
index 8da9dcb..f687e29 100644
--- a/flumi/Scripts/Utils/Lua/Network.gd
+++ b/flumi/Scripts/Utils/Lua/Network.gd
@@ -5,6 +5,27 @@ static func setup_network_api(vm: LuauVM):
vm.lua_pushcallable(_lua_fetch_handler, "fetch")
vm.lua_setglobal("fetch")
+static func resolve_fetch_url(url: String) -> String:
+ # If URL is already absolute, return as-is
+ if url.begins_with("http://") or url.begins_with("https://") or url.begins_with("gurt://"):
+ return url
+
+ # Get current domain from main scene
+ var main_node = Engine.get_main_loop().current_scene
+ var current_domain = ""
+ if main_node and main_node.has_method("get_current_url"):
+ current_domain = main_node.get_current_url()
+
+ # If no current domain, default to gurt:// protocol for relative URLs
+ if current_domain.is_empty():
+ if url.begins_with("/"):
+ return "gurt://" + url.substr(1)
+ else:
+ return "gurt://" + url
+
+ # Use URLUtils to resolve relative URL against current domain
+ return URLUtils.resolve_url(current_domain, url)
+
static func _lua_fetch_handler(vm: LuauVM) -> int:
var url: String = vm.luaL_checkstring(1)
var options: Dictionary = {}
@@ -12,6 +33,9 @@ static func _lua_fetch_handler(vm: LuauVM) -> int:
if vm.lua_gettop() >= 2 and vm.lua_istable(2):
options = vm.lua_todictionary(2)
+ # Resolve relative URLs and default to gurt:// protocol
+ url = resolve_fetch_url(url)
+
# Default options
var method = options.get("method", "GET").to_upper()
var headers = options.get("headers", {})
diff --git a/flumi/Scripts/Utils/URLUtils.gd b/flumi/Scripts/Utils/URLUtils.gd
index 31527a1..9a15dcc 100644
--- a/flumi/Scripts/Utils/URLUtils.gd
+++ b/flumi/Scripts/Utils/URLUtils.gd
@@ -45,11 +45,16 @@ static func resolve_url(base_url: String, relative_url: String) -> String:
# Relative path
final_path_parts = current_path_parts.duplicate()
+ if final_path_parts.size() > 0:
+ var last_part = final_path_parts[-1]
+ if "." in last_part and not last_part.ends_with("/"):
+ final_path_parts.resize(final_path_parts.size() - 1)
+
var href_parts = relative_url.split("/")
for part in href_parts:
if part == "..":
if final_path_parts.size() > 0:
- final_path_parts.pop_back()
+ final_path_parts.resize(final_path_parts.size() - 1)
elif part == "." or part == "":
continue
else:
@@ -59,4 +64,4 @@ static func resolve_url(base_url: String, relative_url: String) -> String:
if final_path_parts.size() > 0:
result += "/" + "/".join(final_path_parts)
- return result
\ No newline at end of file
+ return result
diff --git a/flumi/Scripts/main.gd b/flumi/Scripts/main.gd
index be1dc4c..b537e63 100644
--- a/flumi/Scripts/main.gd
+++ b/flumi/Scripts/main.gd
@@ -86,6 +86,8 @@ func handle_link_click(meta: Variant) -> void:
func _on_search_submitted(url: String) -> void:
print("Search submitted: ", url)
+ search_bar.release_focus()
+
if GurtProtocol.is_gurt_domain(url):
print("Processing as GURT domain")
diff --git a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll
index e7cedfa..6593e22 100644
Binary files a/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll and b/flumi/addons/gurt-protocol/bin/windows/gurt_godot.dll differ
diff --git a/protocol/library/src/client.rs b/protocol/library/src/client.rs
index 73a566d..44c5bcd 100644
--- a/protocol/library/src/client.rs
+++ b/protocol/library/src/client.rs
@@ -105,6 +105,9 @@ impl GurtClient {
let mut temp_buffer = [0u8; 8192];
let start_time = std::time::Instant::now();
+ let mut headers_parsed = false;
+ let mut expected_body_length: Option = None;
+ let mut headers_end_pos: Option = None;
loop {
if start_time.elapsed() > self.config.request_timeout {
@@ -118,12 +121,38 @@ impl GurtClient {
buffer.extend_from_slice(&temp_buffer[..bytes_read]);
- // Check for complete message without converting to string
+ // Check for complete message
let body_separator = BODY_SEPARATOR.as_bytes();
- let has_complete_response = buffer.windows(body_separator.len()).any(|w| w == body_separator) ||
- (buffer.starts_with(b"{") && buffer.ends_with(b"}"));
- if has_complete_response {
+ if !headers_parsed {
+ if let Some(pos) = buffer.windows(body_separator.len()).position(|w| w == body_separator) {
+ headers_end_pos = Some(pos + body_separator.len());
+ headers_parsed = true;
+
+ // Parse headers to get Content-Length
+ let headers_section = &buffer[..pos];
+ if let Ok(headers_str) = std::str::from_utf8(headers_section) {
+ for line in headers_str.lines() {
+ if line.to_lowercase().starts_with("content-length:") {
+ if let Some(length_str) = line.split(':').nth(1) {
+ if let Ok(length) = length_str.trim().parse::() {
+ expected_body_length = Some(length);
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if let (Some(headers_end), Some(expected_length)) = (headers_end_pos, expected_body_length) {
+ let current_body_length = buffer.len() - headers_end;
+ if current_body_length >= expected_length {
+ return Ok(buffer);
+ }
+ } else if headers_parsed && expected_body_length.is_none() {
+ // No Content-Length header, return what we have after headers
return Ok(buffer);
}
}
diff --git a/tests/dom-utils.html b/tests/dom-utils.html
index 27d1740..8c2b699 100644
--- a/tests/dom-utils.html
+++ b/tests/dom-utils.html
@@ -25,21 +25,21 @@
-- Insert Before
gurt.select("#btn-insert-before"):on("click", function()
- local newDiv = gurt.create("div", { class = "test-item highlight", text = "Inserted Before Child 2" })
+ local newDiv = gurt.create("div", { style = "test-item highlight", text = "Inserted Before Child 2" })
parent:insertBefore(newDiv, child2)
log_msg("Inserted before child2: " .. newDiv._element_id)
end)
-- Insert After
gurt.select("#btn-insert-after"):on("click", function()
- local newDiv = gurt.create("div", { class = "test-item highlight", text = "Inserted After Child 2" })
+ local newDiv = gurt.create("div", { style = "test-item highlight", text = "Inserted After Child 2" })
parent:insertAfter(newDiv, child2)
log_msg("Inserted after child2: " .. newDiv._element_id)
end)
-- Replace
gurt.select("#btn-replace"):on("click", function()
- local newDiv = gurt.create("div", { class = "test-item highlight", text = "Replacement for Child 2" })
+ local newDiv = gurt.create("div", { style = "test-item highlight", text = "Replacement for Child 2" })
parent:replace(newDiv, child2)
log_msg("Replaced child2 with: " .. newDiv._element_id)
end)