IC-Webhosting: Domaenen, Webseiten und Postfaecher ueber Anmeldungen

Ein IC-Hostinganbieter im Spiel. Er vergibt Domaenen, richtet je Domaene einen
Zugang ein, und die Inhaber pflegen darauf ihre Webseite.

Die gesamte Verwaltung haengt an einer Anmeldung, nicht an Job oder
Serverrechten. Ein Zugang ist zugleich ein Postfach: wer sich anmeldet,
bekommt das Postfach dieser Adresse. Mehrere Personen koennen denselben Zugang
benutzen und dasselbe Postfach gleichzeitig offen haben - ein Firmenpostfach
gehoert der Firma, nicht einer Person.

Drei Ebenen:
  superadmin  vergibt Domaenen und setzt je Domaene einen Admin ein
  admin       pflegt die Seiten seiner Domaene und legt dort Zugaenge an
  editor      pflegt nur die Seiten

Seiteninhalte sind kein HTML, sondern typisierte Bloecke. Die Seiten werden im
NUI des IC-Computers angezeigt, also im selben Kontext wie dieser selbst - wer
dort Markup einschleusen kann, kann auch dessen Callbacks aufrufen. Ein
Blockmodell schliesst das konstruktiv aus, statt es filtern zu muessen.

Passwoerter liegen gesalzen und gehasht in der Datenbank, gerechnet wird in der
Datenbank. Fuenf Fehlversuche je Benutzername sperren den Namen kurzzeitig.

Enthaelt README.md mit Einrichtung und Anbindung an den IC-Computer sowie
ANLEITUNG.md fuer die Bedienung im Spiel.
This commit is contained in:
Bjoern Flessing 2026-08-09 11:50:53 +00:00
commit a2ce3a69fd
14 changed files with 3018 additions and 0 deletions

411
server/main.lua Normal file
View file

@ -0,0 +1,411 @@
-- ─────────────────────────────────────────────────────────────────────────────
-- ic-web Netzwerkereignisse, Exports, Konsolenbefehle
--
-- Jedes Ereignis ist frei aufrufbar und deshalb gedrosselt. Rechte werden
-- ausnahmslos serverseitig aus der Anmeldung abgeleitet; der Client erfaehrt
-- nur das Ergebnis.
-- ─────────────────────────────────────────────────────────────────────────────
local S = ICWeb.Sites
local D = ICWeb.Domains
local M = ICWeb.Manage
local A = ICWeb.Auth
local DB = ICWeb.DB
local FW = ICWeb.FW
local function reply(src, reqId, ok, data, err)
TriggerClientEvent('ic-web:client:response', src, reqId,
{ ok = ok == true, data = data, error = err })
end
-- ── Anmeldung ────────────────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:login', function(reqId, username, password)
local src = source
-- Eng gedrosselt: die Sperre je Benutzername greift erst nach fuenf
-- Versuchen, diese hier bremst das Durchprobieren vieler Benutzernamen.
if not ICWeb.ThrottleOk(src, 'login', 8, 60000) then
return reply(src, reqId, false, nil, 'Zu viele Versuche, bitte kurz warten.')
end
local session, err = A.Login(src, username, password)
reply(src, reqId, session ~= nil, session, err)
end)
RegisterNetEvent('ic-web:server:logout', function(reqId)
local src = source
A.Logout(src)
reply(src, reqId, true)
end)
-- Wer bin ich gerade? Ohne Anmeldung: data = nil, aber ok = true.
RegisterNetEvent('ic-web:server:session', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'session', 20, 10000) then return end
reply(src, reqId, true, {
session = A.Public(src),
themes = Config.Themes,
tlds = Config.Domain.allowedTlds,
imageHosts = Config.ImageHosts,
provider = Config.MainAdmin.providerName,
selfService = Config.SelfRegistration.enabled,
})
end)
RegisterNetEvent('ic-web:server:changePassword', function(reqId, accountId, password)
local src = source
if not ICWeb.ThrottleOk(src, 'passwd', 5, 60000) then return end
local ok, err = M.SetAccountPassword(src, accountId, password)
if ok then FW.Notify(src, 'Passwort geändert.') end
reply(src, reqId, ok, nil, err)
end)
-- ── Lesen: jeder darf ────────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:getSite', function(reqId, domain, slug)
local src = source
if not ICWeb.ThrottleOk(src, 'get', 30, 10000) then return end
local site, err = S.GetPublic(domain, slug)
reply(src, reqId, site ~= nil, site, err)
end)
-- ── Eigene Domaenen ──────────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:listMine', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'listMine', 10, 10000) then return end
reply(src, reqId, true, S.ListMine(src))
end)
RegisterNetEvent('ic-web:server:getEditable', function(reqId, domain)
local src = source
if not ICWeb.ThrottleOk(src, 'getEditable', 20, 10000) then return end
local site, err = S.GetForEditor(src, domain)
reply(src, reqId, site ~= nil, site, err)
end)
-- ── Selbstanmeldung einer Domaene (nur wenn eingeschaltet) ───────────────────
RegisterNetEvent('ic-web:server:register', function(reqId, domain, title)
local src = source
if not ICWeb.ThrottleOk(src, 'register', 3, 60000) then
return reply(src, reqId, false, nil, 'Zu viele Versuche, bitte kurz warten.')
end
local site, err, credentials = D.Register(src, domain, title)
if not site then
ICWeb.Log.Write(src, 'domain.rejected', { domain = tostring(domain), reason = err }, 'warn')
return reply(src, reqId, false, nil, err)
end
FW.Notify(src, ('Domäne %s registriert.'):format(site.domain))
reply(src, reqId, true, {
domain = site.domain, title = site.title, credentials = credentials,
})
end)
-- ── Bearbeiten ───────────────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:savePage', function(reqId, domain, slug, title, blocks)
local src = source
if not ICWeb.ThrottleOk(src, 'save', 20, 60000) then
return reply(src, reqId, false, nil, 'Zu viele Speichervorgänge, bitte kurz warten.')
end
local ok, err = S.SavePage(src, domain, slug, title, blocks)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:deletePage', function(reqId, domain, slug)
local src = source
if not ICWeb.ThrottleOk(src, 'delete', 10, 60000) then return end
local ok, err = S.DeletePage(src, domain, slug)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:updateSite', function(reqId, domain, data)
local src = source
if not ICWeb.ThrottleOk(src, 'update', 20, 60000) then return end
if type(data) ~= 'table' then return reply(src, reqId, false, nil, 'Ungültige Daten.') end
local ok, err = S.UpdateSite(src, domain, data)
reply(src, reqId, ok, nil, err)
end)
-- ── Melden ───────────────────────────────────────────────────────────────────
-- Ohne Anmeldung: jeder Spieler soll eine Seite melden koennen.
RegisterNetEvent('ic-web:server:report', function(reqId, domain, slug, reason)
local src = source
if not ICWeb.ThrottleOk(src, 'report', 5, 60000) then return end
local ok, err = S.Report(src, domain, slug, reason)
if ok then FW.Notify(src, 'Meldung eingegangen. Danke.') end
reply(src, reqId, ok, nil, err)
end)
-- ── Postfaecher im Mailclient ───────────────────────────────────────────────
-- Ein Postfach freischalten ist eine Passwortpruefung wie die Anmeldung und
-- deshalb genauso eng gedrosselt.
RegisterNetEvent('ic-web:server:openMailbox', function(reqId, username, password)
local src = source
if not ICWeb.ThrottleOk(src, 'openMailbox', 8, 60000) then
return reply(src, reqId, false, nil, 'Zu viele Versuche, bitte kurz warten.')
end
local ok, err = A.OpenMailbox(src, username, password)
if ok then FW.Notify(src, 'Postfach hinzugefügt.') end
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:closeMailbox', function(reqId, username)
local src = source
if not ICWeb.ThrottleOk(src, 'closeMailbox', 20, 60000) then return end
local ok, err = A.CloseMailbox(src, username)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:listMailboxes', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'listMailboxes', 20, 10000) then return end
reply(src, reqId, true, A.ListOpenMailboxes(src))
end)
-- ── Zugaenge je Domaene ─────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:listAccounts', function(reqId, domain)
local src = source
if not ICWeb.ThrottleOk(src, 'accounts', 20, 10000) then return end
local list, err = M.ListAccounts(src, domain)
reply(src, reqId, list ~= nil, list, err)
end)
RegisterNetEvent('ic-web:server:createAccount',
function(reqId, domain, username, password, role, displayName)
local src = source
if not ICWeb.ThrottleOk(src, 'createAccount', 10, 60000) then return end
local account, err = M.CreateAccount(src, domain, username, password, role, displayName)
reply(src, reqId, account ~= nil, account, err)
end)
RegisterNetEvent('ic-web:server:setAccountPassword', function(reqId, accountId, password)
local src = source
if not ICWeb.ThrottleOk(src, 'setPassword', 10, 60000) then return end
local ok, err = M.SetAccountPassword(src, accountId, password)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:setAccountActive', function(reqId, accountId, active)
local src = source
if not ICWeb.ThrottleOk(src, 'setActive', 20, 60000) then return end
local ok, err = M.SetAccountActive(src, accountId, active == true)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:setAccountRole', function(reqId, accountId, role)
local src = source
if not ICWeb.ThrottleOk(src, 'setRole', 20, 60000) then return end
local ok, err = M.SetAccountRole(src, accountId, role)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:deleteAccount', function(reqId, accountId)
local src = source
if not ICWeb.ThrottleOk(src, 'deleteAccount', 10, 60000) then return end
local ok, err = M.DeleteAccount(src, accountId)
reply(src, reqId, ok, nil, err)
end)
-- ── Anbieterebene ───────────────────────────────────────────────────────────
RegisterNetEvent('ic-web:server:adminListSites', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'adminList', 20, 10000) then return end
local list, err = M.AdminListSites(src)
reply(src, reqId, list ~= nil, list, err)
end)
RegisterNetEvent('ic-web:server:adminListAccounts', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'adminAccounts', 20, 10000) then return end
local list, err = M.AdminListAccounts(src)
reply(src, reqId, list ~= nil, list, err)
end)
RegisterNetEvent('ic-web:server:adminListJobs', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'adminJobs', 10, 10000) then return end
local list, err = M.AdminListJobs(src)
reply(src, reqId, list ~= nil, list, err)
end)
RegisterNetEvent('ic-web:server:adminRegister',
function(reqId, domain, title, ownerId, adminUser, adminPassword)
local src = source
if not ICWeb.ThrottleOk(src, 'adminRegister', 10, 60000) then return end
local site, err, credentials = M.AdminRegister(src, domain, title, ownerId,
adminUser, adminPassword)
reply(src, reqId, site ~= nil,
site and { domain = site.domain, credentials = credentials } or nil, err)
end)
RegisterNetEvent('ic-web:server:adminSetBlocked', function(reqId, domain, blocked)
local src = source
if not ICWeb.ThrottleOk(src, 'adminBlock', 20, 60000) then return end
local ok, err = M.AdminSetBlocked(src, domain, blocked == true)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:adminSetOwner', function(reqId, domain, ownerId)
local src = source
if not ICWeb.ThrottleOk(src, 'adminOwner', 10, 60000) then return end
local ok, err = M.AdminSetOwner(src, domain, ownerId)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:adminDeleteSite', function(reqId, domain)
local src = source
if not ICWeb.ThrottleOk(src, 'adminDelete', 5, 60000) then return end
local ok, err = M.AdminDeleteSite(src, domain)
reply(src, reqId, ok, nil, err)
end)
RegisterNetEvent('ic-web:server:adminListReports', function(reqId)
local src = source
if not ICWeb.ThrottleOk(src, 'adminReports', 20, 10000) then return end
local list, err = M.AdminListReports(src)
reply(src, reqId, list ~= nil, list, err)
end)
RegisterNetEvent('ic-web:server:adminSetReportStatus', function(reqId, id, status)
local src = source
if not ICWeb.ThrottleOk(src, 'adminReportStatus', 30, 60000) then return end
local ok, err = M.AdminSetReportStatus(src, id, status)
reply(src, reqId, ok, nil, err)
end)
-- ── Konsolenbefehle ─────────────────────────────────────────────────────────
-- Kein IC-Weg, sondern die Notbremse des Serverbetreibers. Absichtlich nur in
-- der Serverkonsole und fuer Serveradmins nicht ueber die Anmeldung.
local function consoleAllowed(src)
if not src or src == 0 then return true end
return FW.IsAdmin(src)
end
RegisterCommand('webpasswd', function(src, args)
if not consoleAllowed(src) then return end
local username = tostring(args[1] or ''):lower()
local password = tostring(args[2] or '')
if username == '' or password == '' then
print('[ic-web] /webpasswd <benutzer@domaene> <neues-passwort>')
return
end
local account = DB.GetAccountByUsername(username)
if not account then print('[ic-web] Zugang nicht gefunden.') return end
local ok, err = A.SetPassword(account.id, password)
print(ok and ('[ic-web] Passwort von %s geändert.'):format(username)
or ('[ic-web] Fehlgeschlagen: %s'):format(tostring(err)))
end, true)
RegisterCommand('webaccounts', function(src)
if not consoleAllowed(src) then return end
local rows = DB.GetAllAccounts()
print(('[ic-web] %d Zugänge:'):format(#rows))
for _, a in ipairs(rows) do
print((' %-34s %-11s %-22s %s'):format(
a.username, a.role, a.domain or '',
ICWeb.Bool(a.active) and 'aktiv' or 'gesperrt'))
end
end, true)
RegisterCommand('webreports', function(src)
if not consoleAllowed(src) then return end
local reports = DB.GetOpenReports()
if #reports == 0 then print('[ic-web] Keine offenen Meldungen.') return end
print(('[ic-web] %d offene Meldungen:'):format(#reports))
for _, r in ipairs(reports) do
print((' #%d %-24s %-10s %s'):format(
r.id, r.domain, r.page_slug or '-', (r.reason or ''):sub(1, 60)))
end
print('[ic-web] Sperren mit /webblock <domain>, erledigen mit /webreport done <id>')
end, true)
RegisterCommand('webblock', function(src, args)
if not consoleAllowed(src) then return end
local domain = tostring(args[1] or ''):lower()
local site = DB.GetSiteByDomain(domain)
if not site then print('[ic-web] Domäne nicht gefunden.') return end
local blocked = not ICWeb.Bool(site.blocked)
DB.SetBlocked(site.id, blocked)
ICWeb.Log.Write(src, blocked and 'site.blocked' or 'site.unblocked', { domain = domain }, 'warn')
print(('[ic-web] %s ist jetzt %s.'):format(domain, blocked and 'gesperrt' or 'frei'))
end, true)
RegisterCommand('webreport', function(src, args)
if not consoleAllowed(src) then return end
local action = tostring(args[1] or '')
local id = tonumber(args[2])
if (action ~= 'done' and action ~= 'dismiss') or not id then
print('[ic-web] /webreport done <id> oder /webreport dismiss <id>')
return
end
DB.SetReportStatus(id, action == 'done' and 'reviewed' or 'dismissed')
print(('[ic-web] Meldung #%d als %s markiert.'):format(id, action))
end, true)
-- ── Exports fuer andere Resources ────────────────────────────────────────────
exports('GetSite', function(domain, slug)
return S.GetPublic(domain, slug)
end)
exports('DomainExists', function(domain)
return DB.GetSiteByDomain(tostring(domain or ''):lower()) ~= nil
end)
exports('GetSitesForJob', function(job)
return DB.GetSitesForOwner('job', job)
end)
-- Wer ist an diesem Bildschirm angemeldet? pc-live nutzt das, um das Postfach
-- des Zugangs zu oeffnen statt des persoenlichen.
exports('GetSession', function(src)
return A.Public(src)
end)
-- ── Start ────────────────────────────────────────────────────────────────────
AddEventHandler('onResourceStart', function(res)
if res ~= GetCurrentResourceName() then return end
if GetResourceState('ic-mail') ~= 'started' then
print('[ic-web] ^3Hinweis^7: ic-mail läuft nicht Domänen werden ohne Postfächer angelegt.')
end
A.EnsureMainAdmin()
A.CleanupStaleMailAccess()
DB.PruneLoginFails(86400)
local sites = MySQL.single.await('SELECT COUNT(*) AS n FROM ic_web_sites')
print(('[ic-web] bereit. %d Domänen registriert.'):format((sites and sites.n) or 0))
end)