user domains API, .value on input, fix flex sizing
This commit is contained in:
@@ -19,6 +19,7 @@ This is a Domain Management API built with Rust (Actix Web) and PostgreSQL. It p
|
||||
- [GET /auth/me](#get-authme)
|
||||
- [POST /auth/invite](#post-authinvite)
|
||||
- [POST /auth/redeem-invite](#post-authredeem-invite)
|
||||
- [GET /auth/domains](#get-authdomains) 🔒
|
||||
- [Domain Endpoints](#domain-endpoints)
|
||||
- [GET /](#get-)
|
||||
- [POST /domain](#post-domain) 🔒
|
||||
@@ -127,6 +128,50 @@ Redeem an invite code to get 3 additional domain registrations. Requires authent
|
||||
}
|
||||
```
|
||||
|
||||
### GET /auth/domains 🔒
|
||||
|
||||
Get all domains owned by the authenticated user, including their status. Requires `Authorization: Bearer <token>` header.
|
||||
|
||||
**Query Parameters:**
|
||||
- `page` - Page number (default: 1)
|
||||
- `limit` - Items per page (default: 100, max: 1000)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"domains": [
|
||||
{
|
||||
"name": "myawesome",
|
||||
"tld": "dev",
|
||||
"ip": "192.168.1.100",
|
||||
"status": "approved",
|
||||
"denial_reason": null
|
||||
},
|
||||
{
|
||||
"name": "pending",
|
||||
"tld": "fr",
|
||||
"ip": "10.0.0.1",
|
||||
"status": "pending",
|
||||
"denial_reason": null
|
||||
},
|
||||
{
|
||||
"name": "rejected",
|
||||
"tld": "mf",
|
||||
"ip": "172.16.0.1",
|
||||
"status": "denied",
|
||||
"denial_reason": "Invalid IP address"
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"limit": 100
|
||||
}
|
||||
```
|
||||
|
||||
**Status Values:**
|
||||
- `pending` - Domain is awaiting approval
|
||||
- `approved` - Domain has been approved and is active
|
||||
- `denied` - Domain was rejected (see `denial_reason` for details)
|
||||
|
||||
## Domain Endpoints
|
||||
|
||||
### GET /
|
||||
@@ -159,15 +204,6 @@ Submit a domain for approval. Requires authentication and consumes one registrat
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Domain registration submitted for approval",
|
||||
"domain": "myawesome.dev",
|
||||
"status": "pending"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized` - Missing or invalid JWT token
|
||||
- `400 Bad Request` - No registrations remaining, invalid domain, or offensive name
|
||||
|
||||
@@ -26,15 +26,15 @@
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white w-32 h-12
|
||||
}
|
||||
|
||||
.success-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#ef4444] text-white w-32 h-12
|
||||
}
|
||||
|
||||
.danger-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#b91c1c] text-white w-32 h-12
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
@@ -42,7 +42,7 @@
|
||||
}
|
||||
|
||||
.warning-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] text-white w-32 h-12
|
||||
}
|
||||
|
||||
.form-group {
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
||||
p-4 rounded-lg border border-[#dc2626]
|
||||
}
|
||||
|
||||
.domain-item {
|
||||
@@ -98,48 +98,10 @@
|
||||
<div style="stats-card mb-6">
|
||||
<div style="flex justify-between items-center w-full">
|
||||
<p id="user-info" style="text-white text-lg font-semibold">Loading...</p>
|
||||
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="card mb-6">
|
||||
<h2 style="mb-4">Register New Domain</h2>
|
||||
<div style="form-group">
|
||||
<p>Domain Name:</p>
|
||||
<input id="domain-name" type="text" style="form-input" placeholder="myawesome" />
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>Select TLD:</p>
|
||||
<div id="tld-selector" style="tld-selector">
|
||||
<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>
|
||||
<div id="domain-error" style="error-text hidden mb-2"></div>
|
||||
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
||||
</div>
|
||||
|
||||
<div style="card mb-6">
|
||||
<h2>Invite System</h2>
|
||||
<p style="text-[#6b7280] mb-4">Create invite codes to share with friends, or redeem codes to get more domain
|
||||
registrations.</p>
|
||||
|
||||
<p id="invite-code-display" style="invite-code-display mt-2">Placeholder</p>
|
||||
|
||||
<div style="flex flex-col gap-4 items-center justify-center mx-auto">
|
||||
<h3>Create Invite</h3>
|
||||
<button id="create-invite-btn" style="warning-btn">Generate Invite Code</button>
|
||||
</div>
|
||||
<div style="flex flex-col gap-4 mx-auto">
|
||||
<h3>Redeem Invite</h3>
|
||||
<div style="flex gap-2">
|
||||
<input id="invite-code-input" type="text" style="form-input" placeholder="Enter invite code" />
|
||||
<button id="redeem-invite-btn" style="primary-btn">Redeem</button>
|
||||
<button id="new-btn" style="success-btn">New</button>
|
||||
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||
</div>
|
||||
<div id="redeem-error" style="error-text hidden mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
local user = nil
|
||||
local domains = {}
|
||||
local tlds = {}
|
||||
local authToken = nil
|
||||
|
||||
local userInfo = gurt.select('#user-info')
|
||||
local domainsList = gurt.select('#domains-list')
|
||||
local tldSelector = gurt.select('#tld-selector')
|
||||
local loadingElement = gurt.select('#tld-loading')
|
||||
local displayElement = gurt.select('#invite-code-display')
|
||||
local options
|
||||
|
||||
displayElement:hide()
|
||||
local options
|
||||
|
||||
local function showError(elementId, message)
|
||||
local element = gurt.select('#' .. elementId)
|
||||
@@ -29,57 +25,17 @@ local function updateUserInfo()
|
||||
userInfo.text = 'Welcome, ' .. user.username .. '!'
|
||||
end
|
||||
|
||||
local function renderTLDSelector()
|
||||
loadingElement:remove()
|
||||
|
||||
tldSelector.text = ''
|
||||
local i = 1
|
||||
local total = #tlds
|
||||
local intervalId
|
||||
|
||||
intervalId = gurt.setInterval(function()
|
||||
if i > total then
|
||||
gurt.clearInterval(intervalId)
|
||||
return
|
||||
end
|
||||
|
||||
local tld = tlds[i]
|
||||
local option = gurt.create('button', {
|
||||
text = '.' .. tld,
|
||||
style = 'tld-option',
|
||||
['data-tld'] = tld
|
||||
})
|
||||
|
||||
tldSelector:append(option)
|
||||
|
||||
option:on('click', function()
|
||||
-- Clear previous selection
|
||||
if not options then
|
||||
options = gurt.selectAll('.tld-option')
|
||||
end
|
||||
|
||||
for j = 1, #options do
|
||||
if options[j].classList:contains('tld-selected') then
|
||||
options[j].classList:remove('tld-selected')
|
||||
end
|
||||
end
|
||||
|
||||
-- Select this option
|
||||
option.classList:add('tld-selected')
|
||||
end)
|
||||
i = i + 1
|
||||
end, 16)
|
||||
end
|
||||
|
||||
local function renderDomains()
|
||||
local loadingElement = gurt.select('#domains-loading')
|
||||
loadingElement:remove()
|
||||
if loadingElement then
|
||||
loadingElement:remove()
|
||||
end
|
||||
|
||||
domainsList.text = ''
|
||||
|
||||
if #domains == 0 then
|
||||
local emptyMessage = gurt.create('div', {
|
||||
text = 'No domains registered yet. Submit your first domain below!',
|
||||
text = 'No domains registered yet. Click "New" to register your first domain!',
|
||||
style = 'text-center text-[#6b7280] py-8'
|
||||
})
|
||||
domainsList:append(emptyMessage)
|
||||
@@ -91,7 +47,7 @@ local function renderDomains()
|
||||
style = 'domain-item'
|
||||
})
|
||||
|
||||
local domainInfo = gurt.create('div', {})
|
||||
local domainInfo = gurt.create('div', { style = 'w-full' })
|
||||
|
||||
local domainName = gurt.create('div', {
|
||||
text = domain.name .. '.' .. domain.tld,
|
||||
@@ -103,8 +59,14 @@ local function renderDomains()
|
||||
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'
|
||||
@@ -143,7 +105,7 @@ end
|
||||
|
||||
local function loadDomains()
|
||||
print('Loading domains...')
|
||||
local response = fetch('gurt://localhost:8877/domains?page=1&size=100', {
|
||||
local response = fetch('gurt://localhost:8877/auth/domains?page=1&size=100', {
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
@@ -159,19 +121,6 @@ local function loadDomains()
|
||||
end
|
||||
end
|
||||
|
||||
local function loadTLDs()
|
||||
print('Loading available TLDs...')
|
||||
local response = fetch('gurt://localhost:8877/tlds')
|
||||
|
||||
if response:ok() then
|
||||
tlds = response:json()
|
||||
print('Loaded ' .. #tlds .. ' TLDs')
|
||||
renderTLDSelector()
|
||||
else
|
||||
print('Failed to load TLDs: ' .. response:text())
|
||||
end
|
||||
end
|
||||
|
||||
local function checkAuth()
|
||||
authToken = gurt.crumbs.get("auth_token")
|
||||
|
||||
@@ -188,7 +137,6 @@ local function checkAuth()
|
||||
print('Authentication successful for user: ' .. user.username)
|
||||
updateUserInfo()
|
||||
loadDomains()
|
||||
loadTLDs()
|
||||
else
|
||||
print('Token invalid, redirecting to login...')
|
||||
gurt.crumbs.delete('auth_token')
|
||||
@@ -206,126 +154,13 @@ local function logout()
|
||||
gurt.location.goto("../")
|
||||
end
|
||||
|
||||
local function submitDomain(name, tld, ip)
|
||||
hideError('domain-error')
|
||||
print('Submitting domain: ' .. name .. '.' .. tld)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/domain', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({ name = name, tld = tld, ip = ip })
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
local data = response:json()
|
||||
print('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)
|
||||
print('Domain submission failed: ' .. error)
|
||||
end
|
||||
end
|
||||
|
||||
local function createInvite()
|
||||
print('Creating invite code...')
|
||||
local response = fetch('gurt://localhost:8877/auth/invite', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
local data = response:json()
|
||||
local inviteCode = data.invite_code
|
||||
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
||||
displayElement:show()
|
||||
Clipboard.write(inviteCode)
|
||||
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
||||
else
|
||||
print('Failed to create invite: ' .. response:text())
|
||||
end
|
||||
end
|
||||
|
||||
local function redeemInvite(code)
|
||||
hideError('redeem-error')
|
||||
print('Redeeming invite code: ' .. code)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/auth/redeem-invite', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({ invite_code = code })
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
local data = response:json()
|
||||
print('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)
|
||||
print('Failed to redeem invite: ' .. error)
|
||||
end
|
||||
local function goToRegister()
|
||||
gurt.location.goto("/register.html")
|
||||
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('#new-btn'):on('click', goToRegister)
|
||||
|
||||
-- Initialize
|
||||
print('Dashboard initialized')
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
#log-output { text-white p-4 rounded-md mt-4 font-mono max-h-40 }
|
||||
</style>
|
||||
|
||||
<script src="script.lua" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
136
dns/frontend/register.html
Normal file
136
dns/frontend/register.html
Normal file
@@ -0,0 +1,136 @@
|
||||
<head>
|
||||
<title>Register Domain</title>
|
||||
<icon src="https://cdn-icons-png.flaticon.com/512/295/295128.png">
|
||||
<meta name="theme-color" content="#0891b2">
|
||||
<meta name="description" content="Register a new 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
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#525252] text-white w-32 h-12
|
||||
}
|
||||
|
||||
.warning-btn {
|
||||
p-3 rounded-lg font-medium cursor-pointer transition-colors bg-[#dc2626] 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
|
||||
}
|
||||
|
||||
.card {
|
||||
bg-[#262626] p-6 rounded-lg shadow border border-gray-700
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
bg-[#1f1f1f] p-4 rounded-lg border border-[#dc2626]
|
||||
}
|
||||
|
||||
.error-text {
|
||||
text-[#fca5a5] text-sm
|
||||
}
|
||||
|
||||
.tld-selector {
|
||||
flex flex-wrap gap-2
|
||||
}
|
||||
|
||||
.tld-option {
|
||||
px-3 py-1 rounded border border-gray-600 cursor-pointer bg-[#374151] text-white w-12 h-12
|
||||
}
|
||||
|
||||
.tld-selected {
|
||||
bg-[#dc2626] text-white
|
||||
}
|
||||
|
||||
.invite-code-display {
|
||||
bg-[#374151] p-3 rounded font-mono text-center mb-2 text-white
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="register.lua" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div style="container mt-6">
|
||||
<div style="stats-card mb-6">
|
||||
<div style="flex justify-between items-center w-full">
|
||||
<p id="user-info" style="text-white text-lg font-semibold">Loading...</p>
|
||||
<div style="flex gap-2">
|
||||
<button id="dashboard-btn" style="secondary-btn">Dashboard</button>
|
||||
<button id="logout-btn" style="secondary-btn">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="card mb-6">
|
||||
<h2 id="remaining" style="mb-4">Register New Domain</h2>
|
||||
<div style="form-group">
|
||||
<p>Domain Name:</p>
|
||||
<input id="domain-name" type="text" style="form-input" placeholder="myawesome" />
|
||||
</div>
|
||||
<div style="form-group">
|
||||
<p>Select TLD:</p>
|
||||
<div id="tld-selector" style="tld-selector">
|
||||
<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>
|
||||
<div id="domain-error" style="error-text hidden mb-2"></div>
|
||||
<button id="submit-domain-btn" style="success-btn">Submit for Approval</button>
|
||||
</div>
|
||||
|
||||
<div style="card mb-6">
|
||||
<h2>Invite System</h2>
|
||||
<p style="text-[#6b7280] mb-4">Create invite codes to share with friends, or redeem codes to get more domain registrations.</p>
|
||||
|
||||
<p id="invite-code-display" style="invite-code-display mt-2">Placeholder</p>
|
||||
|
||||
<div style="flex flex-col gap-4 items-center justify-center mx-auto">
|
||||
<h3>Create Invite</h3>
|
||||
<button id="create-invite-btn" style="warning-btn">Generate Invite Code</button>
|
||||
</div>
|
||||
<div style="flex flex-col gap-4 mx-auto">
|
||||
<h3>Redeem Invite</h3>
|
||||
<div style="flex gap-2">
|
||||
<input id="invite-code-input" type="text" style="form-input" placeholder="Enter invite code" />
|
||||
<button id="redeem-invite-btn" style="primary-btn">Redeem</button>
|
||||
</div>
|
||||
<div id="redeem-error" style="error-text hidden mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
257
dns/frontend/register.lua
Normal file
257
dns/frontend/register.lua
Normal file
@@ -0,0 +1,257 @@
|
||||
local user = nil
|
||||
local tlds = {}
|
||||
local authToken = nil
|
||||
|
||||
local userInfo = gurt.select('#user-info')
|
||||
local tldSelector = gurt.select('#tld-selector')
|
||||
local loadingElement = gurt.select('#tld-loading')
|
||||
local displayElement = gurt.select('#invite-code-display')
|
||||
local remainingElement = gurt.select('#remaining')
|
||||
|
||||
local options
|
||||
|
||||
displayElement:hide()
|
||||
|
||||
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 updateUserInfo()
|
||||
userInfo.text = 'Welcome, ' .. user.username .. '!'
|
||||
remainingElement.text = 'Register New Domain (' .. user.registrations_remaining .. ' remaining)'
|
||||
end
|
||||
|
||||
local function renderTLDSelector()
|
||||
loadingElement:remove()
|
||||
|
||||
tldSelector.text = ''
|
||||
local i = 1
|
||||
local total = #tlds
|
||||
local intervalId
|
||||
|
||||
intervalId = gurt.setInterval(function()
|
||||
if i > total then
|
||||
gurt.clearInterval(intervalId)
|
||||
return
|
||||
end
|
||||
|
||||
local tld = tlds[i]
|
||||
local option = gurt.create('button', {
|
||||
text = '.' .. tld,
|
||||
style = 'tld-option',
|
||||
['data-tld'] = tld
|
||||
})
|
||||
|
||||
tldSelector:append(option)
|
||||
|
||||
option:on('click', function()
|
||||
-- Clear previous selection
|
||||
if not options then
|
||||
options = gurt.selectAll('.tld-option')
|
||||
end
|
||||
|
||||
for j = 1, #options do
|
||||
if options[j].classList:contains('tld-selected') then
|
||||
options[j].classList:remove('tld-selected')
|
||||
end
|
||||
end
|
||||
|
||||
-- Select this option
|
||||
option.classList:add('tld-selected')
|
||||
end)
|
||||
i = i + 1
|
||||
end, 16)
|
||||
end
|
||||
|
||||
local function loadTLDs()
|
||||
print('Loading available TLDs...')
|
||||
local response = fetch('gurt://localhost:8877/tlds')
|
||||
|
||||
if response:ok() then
|
||||
tlds = response:json()
|
||||
print('Loaded ' .. #tlds .. ' TLDs')
|
||||
renderTLDSelector()
|
||||
else
|
||||
print('Failed to load TLDs: ' .. response:text())
|
||||
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)
|
||||
updateUserInfo()
|
||||
loadTLDs()
|
||||
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 logout()
|
||||
gurt.crumbs.delete('auth_token')
|
||||
print('Logged out successfully')
|
||||
gurt.location.goto("../")
|
||||
end
|
||||
|
||||
local function goToDashboard()
|
||||
gurt.location.goto("/dashboard.html")
|
||||
end
|
||||
|
||||
local function submitDomain(name, tld, ip)
|
||||
hideError('domain-error')
|
||||
print('Submitting domain: ' .. name .. '.' .. tld)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/domain', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({ name = name, tld = tld, ip = ip })
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
print('Domain submitted successfully.')
|
||||
|
||||
-- Update user registrations remaining
|
||||
user.registrations_remaining = user.registrations_remaining - 1
|
||||
updateUserInfo()
|
||||
|
||||
-- Clear form
|
||||
gurt.select('#domain-name').text = ''
|
||||
gurt.select('#domain-ip').text = ''
|
||||
|
||||
-- Redirect to dashboard
|
||||
gurt.location.goto('/dashboard.html')
|
||||
else
|
||||
local error = response:text()
|
||||
showError('domain-error', 'Domain submission failed: ' .. error)
|
||||
print('Domain submission failed: ' .. error)
|
||||
end
|
||||
end
|
||||
|
||||
local function createInvite()
|
||||
print('Creating invite code...')
|
||||
local response = fetch('gurt://localhost:8877/auth/invite', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
}
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
local data = response:json()
|
||||
local inviteCode = data.invite_code
|
||||
displayElement.text = 'Invite code: ' .. inviteCode .. ' (copied to clipboard)'
|
||||
displayElement:show()
|
||||
Clipboard.write(inviteCode)
|
||||
print('Invite code created and copied to clipboard: ' .. inviteCode)
|
||||
else
|
||||
print('Failed to create invite: ' .. response:text())
|
||||
end
|
||||
end
|
||||
|
||||
local function redeemInvite(code)
|
||||
hideError('redeem-error')
|
||||
print('Redeeming invite code: ' .. code)
|
||||
|
||||
local response = fetch('gurt://localhost:8877/auth/redeem-invite', {
|
||||
method = 'POST',
|
||||
headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
Authorization = 'Bearer ' .. authToken
|
||||
},
|
||||
body = JSON.stringify({ invite_code = code })
|
||||
})
|
||||
|
||||
if response:ok() then
|
||||
local data = response:json()
|
||||
print('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)
|
||||
print('Failed to redeem invite: ' .. error)
|
||||
end
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
gurt.select('#logout-btn'):on('click', logout)
|
||||
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
|
||||
print('Validation failed: Domain name is required')
|
||||
showError('domain-error', 'Domain name is required')
|
||||
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')
|
||||
return
|
||||
end
|
||||
|
||||
local tld = selectedTLD:getAttribute('data-tld')
|
||||
print('Submitting domain with name:', name, 'tld:', tld, 'ip:', ip)
|
||||
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)
|
||||
|
||||
-- Initialize
|
||||
print('Register page initialized')
|
||||
checkAuth()
|
||||
@@ -93,6 +93,7 @@ enum HandlerType {
|
||||
CreateDomain,
|
||||
UpdateDomain,
|
||||
DeleteDomain,
|
||||
GetUserDomains,
|
||||
}
|
||||
|
||||
impl GurtHandler for AppHandler {
|
||||
@@ -128,6 +129,7 @@ impl GurtHandler for AppHandler {
|
||||
HandlerType::RedeemInvite => handle_authenticated!(ctx, app_state, auth_routes::redeem_invite),
|
||||
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::CreateDomain => {
|
||||
// Check rate limit first
|
||||
if let Some(ref rate_limit_state) = rate_limit_state {
|
||||
@@ -205,6 +207,7 @@ pub async fn start(cli: crate::Cli) -> std::io::Result<()> {
|
||||
.route(Route::post("/auth/redeem-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::RedeemInvite })
|
||||
.route(Route::post("/auth/create-domain-invite"), AppHandler { app_state: app_state.clone(), rate_limit_state: None, handler_type: HandlerType::CreateDomainInvite })
|
||||
.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::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 });
|
||||
|
||||
@@ -101,3 +101,19 @@ pub(crate) struct DomainList {
|
||||
pub(crate) domain: String,
|
||||
pub(crate) taken: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
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>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct UserDomainResponse {
|
||||
pub(crate) domains: Vec<UserDomain>,
|
||||
pub(crate) page: u32,
|
||||
pub(crate) limit: u32,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub(crate) async fn create_logic(domain: Domain, user_id: i32, app: &AppState) -
|
||||
}
|
||||
|
||||
let existing_count: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM domains WHERE name = ? AND tld = ?"
|
||||
"SELECT COUNT(*) FROM domains WHERE name = $1 AND tld = $2"
|
||||
)
|
||||
.bind(&domain.name)
|
||||
.bind(&domain.tld)
|
||||
@@ -306,6 +306,56 @@ pub(crate) async fn delete_domain(ctx: &ServerContext, app_state: AppState, clai
|
||||
Ok(GurtResponse::ok().with_string_body("Domain deleted successfully"))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_user_domains(ctx: &ServerContext, app_state: AppState, claims: Claims) -> Result<GurtResponse> {
|
||||
// Parse pagination from query parameters
|
||||
let path = ctx.path();
|
||||
let query_params = if let Some(query_start) = path.find('?') {
|
||||
let query_string = &path[query_start + 1..];
|
||||
parse_query_string(query_string)
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
|
||||
let page = query_params.get("page")
|
||||
.and_then(|p| p.parse::<u32>().ok())
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
|
||||
let page_size = query_params.get("limit")
|
||||
.and_then(|l| l.parse::<u32>().ok())
|
||||
.unwrap_or(100)
|
||||
.clamp(1, 1000);
|
||||
|
||||
let offset = (page - 1) * page_size;
|
||||
|
||||
let domains: Vec<Domain> = sqlx::query_as::<_, Domain>(
|
||||
"SELECT id, name, tld, ip, user_id, status, denial_reason, created_at FROM domains WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"
|
||||
)
|
||||
.bind(claims.user_id)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(&app_state.db)
|
||||
.await
|
||||
.map_err(|_| GurtError::invalid_message("Database error"))?;
|
||||
|
||||
let response_domains: Vec<UserDomain> = domains.into_iter().map(|domain| {
|
||||
UserDomain {
|
||||
name: domain.name,
|
||||
tld: domain.tld,
|
||||
ip: domain.ip,
|
||||
status: domain.status.unwrap_or_else(|| "pending".to_string()),
|
||||
denial_reason: domain.denial_reason,
|
||||
}
|
||||
}).collect();
|
||||
|
||||
let response = UserDomainResponse {
|
||||
domains: response_domains,
|
||||
page,
|
||||
limit: page_size,
|
||||
};
|
||||
|
||||
Ok(GurtResponse::ok().with_json_body(&response)?)
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct Error {
|
||||
|
||||
Reference in New Issue
Block a user