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

204
server/sites.lua Normal file
View file

@ -0,0 +1,204 @@
-- ─────────────────────────────────────────────────────────────────────────────
-- ic-web Seiten lesen und schreiben, Meldungen
-- ─────────────────────────────────────────────────────────────────────────────
ICWeb = ICWeb or {}
ICWeb.Sites = {}
local S = ICWeb.Sites
local DB = ICWeb.DB
local FW = ICWeb.FW
local D = ICWeb.Domains
local B = ICWeb.Blocks
-- Bloecke aus der Datenbank lesen. Kaputtes JSON darf keine Seite zerlegen.
local function decodeBlocks(raw)
if type(raw) ~= 'string' or raw == '' then return {} end
local ok, decoded = pcall(json.decode, raw)
if not ok or type(decoded) ~= 'table' then return {} end
return decoded
end
-- ── Lesen ───────────────────────────────────────────────────────────────────
-- Was der Browser bekommt. Gibt (site, nil) oder (nil, fehlertext) zurueck.
function S.GetPublic(domain, slug)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return nil, 'Diese Adresse existiert nicht.' end
if ICWeb.Bool(site.blocked) then return nil, 'Diese Seite wurde gesperrt.' end
if not ICWeb.Bool(site.published) then
return nil, 'Diese Seite ist noch nicht veröffentlicht.'
end
local pages = DB.GetPages(site.id)
if #pages == 0 then return nil, 'Diese Seite hat noch keinen Inhalt.' end
slug = tostring(slug or 'home')
local page
for _, p in ipairs(pages) do
if p.slug == slug then page = p break end
end
page = page or pages[1]
-- Navigation: nur Titel und Slug, keine Inhalte.
local nav = {}
for _, p in ipairs(pages) do
nav[#nav + 1] = { slug = p.slug, title = p.title }
end
return {
domain = site.domain,
title = site.title,
theme = site.theme,
nav = nav,
page = { slug = page.slug, title = page.title, blocks = decodeBlocks(page.blocks) },
}, nil
end
-- Was der Bearbeiter bekommt: auch unveroeffentlichte Seiten.
function S.GetForEditor(src, domain)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return nil, 'Domäne nicht gefunden.' end
if not D.CanEdit(src, site) then return nil, 'Keine Berechtigung für diese Domäne.' end
local pages = {}
for _, p in ipairs(DB.GetPages(site.id)) do
pages[#pages + 1] = {
slug = p.slug, title = p.title,
sort_order = p.sort_order, blocks = decodeBlocks(p.blocks),
}
end
return {
id = site.id, domain = site.domain, title = site.title,
theme = site.theme, published = ICWeb.Bool(site.published),
blocked = ICWeb.Bool(site.blocked), pages = pages,
}, nil
end
-- Alle Domaenen, die der angemeldete Zugang bearbeiten darf.
function S.ListMine(src)
local session = ICWeb.Auth.Get(src)
if not session then return {} end
local function entry(site)
return { domain = site.domain, title = site.title,
published = ICWeb.Bool(site.published),
blocked = ICWeb.Bool(site.blocked) }
end
-- Der Anbieter sieht alles, alle anderen genau ihre Domaene.
if session.role == 'superadmin' then
local out = {}
for _, site in ipairs(DB.GetAllSites()) do out[#out + 1] = entry(site) end
return out
end
local site = session.domain and DB.GetSiteByDomain(session.domain)
return site and { entry(site) } or {}
end
-- ── Schreiben ───────────────────────────────────────────────────────────────
local function checkSlug(slug)
slug = tostring(slug or ''):lower():gsub('%s', '')
if not slug:match('^[a-z0-9%-]+$') then return nil end
if #slug < 1 or #slug > 60 then return nil end
return slug
end
-- Seite speichern. Gibt (true, nil) oder (false, fehlertext) zurueck.
function S.SavePage(src, domain, slug, title, blocks)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
if not D.CanEdit(src, site) then return false, 'Keine Berechtigung.' end
if ICWeb.Bool(site.blocked) then return false, 'Diese Seite ist gesperrt.' end
local safeSlug = checkSlug(slug)
if not safeSlug then return false, 'Ungültiger Seitenname.' end
-- Neue Seite? Dann Obergrenze pruefen.
if not DB.GetPage(site.id, safeSlug) then
if DB.CountPages(site.id) >= (Config.MaxPagesPerSite or 8) then
return false, ('Höchstens %d Seiten je Domäne.'):format(Config.MaxPagesPerSite or 8)
end
end
-- Hier faellt alles weg, was nicht ins Blockschema passt.
local safeBlocks, err = B.ValidatePage(blocks)
if not safeBlocks then return false, err end
local safeTitle = tostring(title or safeSlug):sub(1, 120)
DB.SavePage(site.id, safeSlug, safeTitle, json.encode(safeBlocks), 0)
ICWeb.Log.Write(src, 'page.saved', {
domain = site.domain, slug = safeSlug, blocks = #safeBlocks,
})
return true, nil
end
function S.DeletePage(src, domain, slug)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
if not D.CanEdit(src, site) then return false, 'Keine Berechtigung.' end
local safeSlug = checkSlug(slug)
if not safeSlug then return false, 'Ungültiger Seitenname.' end
if safeSlug == 'home' then return false, 'Die Startseite lässt sich nicht löschen.' end
DB.DeletePage(site.id, safeSlug)
ICWeb.Log.Write(src, 'page.deleted', { domain = site.domain, slug = safeSlug })
return true, nil
end
-- Titel, Theme und Veroeffentlichung.
function S.UpdateSite(src, domain, data)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
if not D.CanEdit(src, site) then return false, 'Keine Berechtigung.' end
if ICWeb.Bool(site.blocked) then return false, 'Diese Seite ist gesperrt.' end
local theme = tostring(data.theme or site.theme)
local themeOk = false
for _, t in ipairs(Config.Themes) do
if t == theme then themeOk = true break end
end
if not themeOk then theme = Config.DefaultTheme end
DB.UpdateSite(site.id, {
title = tostring(data.title or site.title):sub(1, 120),
theme = theme,
published = data.published == true,
})
ICWeb.Log.Write(src, 'site.updated', {
domain = site.domain, published = data.published == true, theme = theme,
})
return true, nil
end
-- ── Meldungen ───────────────────────────────────────────────────────────────
-- Bilder werden extern verlinkt; die Hostliste faengt nur den groben Rahmen ab.
-- Deshalb muss jeder Spieler eine Seite melden koennen.
function S.Report(src, domain, slug, reason)
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
local identifier = FW.GetIdentifier(src)
if not identifier then return false, 'Spieler nicht geladen.' end
-- Eine Meldung je Spieler und Seite pro Stunde: sonst laesst sich die
-- Meldeliste durch Wiederholung unbrauchbar machen.
if DB.RecentReportBy(site.id, identifier, 3600) then
return false, 'Du hast diese Seite bereits gemeldet.'
end
local safeReason = tostring(reason or ''):gsub('[^\32-\126\128-\255]', ''):sub(1, 500)
DB.AddReport(site.id, checkSlug(slug), safeReason, identifier)
ICWeb.Log.Write(src, 'site.reported', {
domain = site.domain, slug = slug, reason = safeReason,
}, 'warn')
return true, nil
end