This guide will take you from zero webserver knowledge, to having your own free site hosted on GitHub. The only non-free bit is registering a domain name, which can be as cheap as $5/year.
This guide uses CrazyDomains as the Registrar, FreeDNS as the DNS, and GitHub as the Webserver.
The Domain Name Registrar is where you register your domain name, This is the only not free step.
Your registrar needs to know which DNS Server will associate the Domain Name with a WebServer.
I've got mine with CrazyDomains, as they offer the cheapest .com.au domains.
- Create an account, chuck in your payment details, etc.
- Register your domain(s)
- Set your domains
Name Serverstons1.afraid.org, ns2, ns3, and ns4.
Edit: Ignore this section and use CloudFlare instead. It's free, it's a DNS, It's also a CDN, and it's also easier.
I use FreeDNS, cause it's free, and doesn't look too dodgy.
- Set up an account, login
- Go to
Domains->Add a Domain to FreeDNSDomain: yourdomain.comShared State:Shared: Public - Go to
Domains-> yourdomain.com ->Manage - Click the
Addbutton at the top-rightType:ASubdomain: blankDomain: yourdomain.com (public)(broken!)Destination:192.30.252.153Wildcard: ticked - Click the
Addbutton next to yourdomain.com again, same settings as last time, but this time use the ip:192.30.252.154
- go to GitHub.com, create an account, and login
- Create a new Repository, mame it yourdomain.com, leave it as
Public, and tick `Initialize this repository with a README`` - Click
Settings(in the right-middle -- for the repo, not for your account) - Under
Github PagesclickAutomatic page generator - Make any changes you like, put in a Google Analytics ID if you want awesome free stats, click
Continue to layouts - Choose a theme, or just go with the default and change it later. Click
Publish Page - Your page can now be viewed at
http://<your github username>.github.io/<your repo name>/ - Back on your repo's page, click
Branch: masterand change to thegh-pagesbranch - Next to the Branch selector, it will show yourdomain.com, followed by a
/and a+. Click the+to create a new file. - Name the file
CNAMEand put yourdomain.com inside as the contents, clickCommit new fileto save it.
--
That's it! Everything's set up!
It can take up to 24 hours for the Domain Name to propogate, sometimes it only takes a few minutes thoughu. Until then browsing to yourdomain.com will just return an error. If you're really impatient you can add it to your computer's hosts file, otherwise just wait it out.
- Settings -> Default branch:
gh-pages
User not found.
`; loadKeysBtnText.classList.remove("hidden"); loadKeysLoader.classList.add("hidden"); loadKeysBtn.disabled = false; return; } try { const response = await fetch(`${API_URL}/keys?username=${currentAdminUsername}&isSuper=${isSuper}`); const keysArray = await response.json(); allKeysData = keysArray; if (keysArray.length === 0) { const msg = isSuper ? "No keys found." : "You haven't created keys."; keysTableContainer.innerHTML = `${msg}
`; return; } let html = ``; if (isSuper) html += ``; html += ``; keysArray.forEach(data => { const keyVal = data.key_value || 'N/A'; const buyerVal = data.buyer_name || 'N/A'; const expiryVal = data.expiry_date || 'N/A'; const createdByVal = data.created_by || 'N/A'; const expired = isDateExpired(expiryVal); const statusClass = expired ? 'status-expired' : 'status-active'; const statusText = expired ? 'Expired' : 'Active'; html += ``; if (isSuper) html += ``; html += ``; }); html += `Error: ${e.message}
`; } finally { loadKeysBtnText.classList.remove("hidden"); loadKeysLoader.classList.add("hidden"); loadKeysBtn.disabled = false; } } keysTableContainer.addEventListener('click', async (e) => { const target = e.target.closest('button'); if (!target) return; const keyId = target.dataset.id; if (!keyId) return; if (target.classList.contains('delete-key-btn')) { const confirmed = await showConfirmation({ title: "Delete Key?", body: "Are you sure?", confirmText: "Delete", confirmColor: "bg-red-600" }); if (!confirmed) return; try { const response = await fetch(`${API_URL}/keys/${keyId}`, { method: 'DELETE' }); const data = await response.json(); if (!response.ok) showToast("Error: " + data.message, true); else { showToast("Key deleted!"); loadAllClientKeys(); loadDashboardData(); } } catch (e) { console.error("Key Del Error: ", e); showToast("Error: " + e.message, true); } } else if (target.classList.contains('edit-key-btn')) { openEditModal(keyId); } }); async function openEditModal(keyId) { const keyData = allKeysData.find(k => k._id === keyId); if (keyData) { editKeyIdInput.value = keyId; editBuyerNameInput.value = keyData.buyer_name || ''; editExpiryDateInput.value = keyData.expiry_date || ''; editDeviceLimitInput.value = keyData.device_limit || ''; editKeyModal.classList.add('show'); } else { showToast("Key not found!", true); } } function closeEditModal() { editKeyModal.classList.remove('show'); } cancelEditBtn.addEventListener('click', closeEditModal); saveEditBtn.addEventListener('click', async () => { const keyId = editKeyIdInput.value; const newBuyerName = editBuyerNameInput.value; const newExpiryDate = editExpiryDateInput.value; const newDeviceLimit = parseInt(editDeviceLimitInput.value); if (!keyId || !newBuyerName || !newExpiryDate || isNaN(newDeviceLimit)) { showToast("Fill fields correctly.", true); return; } saveEditBtn.textContent = 'Saving...'; saveEditBtn.disabled = true; try { const response = await fetch(`${API_URL}/keys/${keyId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ buyer_name: newBuyerName, expiry_date: newExpiryDate, device_limit: newDeviceLimit }) }); const data = await response.json(); if (!response.ok) showToast("Error: " + data.message, true); else { showToast("Key updated!"); closeEditModal(); loadAllClientKeys(); loadDashboardData(); } } catch (e) { console.error("Key Edit Error:", e); showToast("Error: " + e.message, true); } finally { saveEditBtn.textContent = 'Save Changes'; saveEditBtn.disabled = false; } }); // --- MANAGE RESELLERS LOGIC --- // [JAVASCRIPT - NO CHANGES] loadResellersBtn.addEventListener('click', loadAllResellers); async function loadAllResellers() { loadResellersBtnText.classList.add("hidden"); loadResellersLoader.classList.remove("hidden"); loadResellersBtn.disabled = true; try { const response = await fetch(`${API_URL}/resellers`); const resellersArray = await response.json(); if (resellersArray.length === 0) { resellersTableContainer.innerHTML = `No resellers found.
`; return; } let html = ``; resellersArray.forEach(data => { const username = data.username || 'N/A'; const credits = data.credits || 0; html += ``; }); html += `No unused codes.
`; return; } let html = ``; codesArray.forEach(data => { html += ``; }); html += `