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

220
server/domains.lua Normal file
View file

@ -0,0 +1,220 @@
-- ─────────────────────────────────────────────────────────────────────────────
-- ic-web Domaenen: Registrierung, Besitz, Bearbeitungsrecht
--
-- Domaenen vergibt der Anbieter ueber die Verwaltung (ICWeb.Manage). Die
-- Selbstanmeldung ueber den ESX-Job weiter unten ist der aeltere Weg und
-- standardmaessig ausgeschaltet siehe Config.SelfRegistration.
--
-- Die Domaene selbst wird in ic-mail angelegt. Dadurch bekommt eine Firma mit
-- der Webseite automatisch auch die passende Mail-Domaene, und es kann nicht
-- passieren, dass zwei Systeme dieselbe Domaene verschiedenen Besitzern geben.
-- ─────────────────────────────────────────────────────────────────────────────
ICWeb = ICWeb or {}
ICWeb.Domains = {}
local D = ICWeb.Domains
local DB = ICWeb.DB
local FW = ICWeb.FW
-- ── Namenspruefung ──────────────────────────────────────────────────────────
-- Gibt (domain, nil) oder (nil, fehlertext) zurueck.
--
-- allowReserved hebt nur die Namensliste auf, nicht die Formregeln. Sie soll
-- verhindern, dass Spieler sich Behoerdennamen greifen; der Anbieter vergibt
-- diese Namen ja gerade absichtlich. Der technische Schutz Domaenen des
-- Mailsystems haengt nicht hier, sondern in D.EnsureRealm und gilt fuer alle.
function D.CheckName(raw, allowReserved)
if type(raw) ~= 'string' then return nil, 'kein Name angegeben' end
local domain = raw:lower():gsub('%s', '')
local name, tld = domain:match('^([a-z0-9%-]+)%.([a-z]+)$')
if not name then
return nil, 'Format: name.endung, nur Buchstaben, Ziffern und Bindestrich'
end
if #name < Config.Domain.minLength or #name > Config.Domain.maxLength then
return nil, ('Name muss %d bis %d Zeichen lang sein')
:format(Config.Domain.minLength, Config.Domain.maxLength)
end
if name:sub(1, 1) == '-' or name:sub(-1) == '-' then
return nil, 'Name darf nicht mit Bindestrich beginnen oder enden'
end
local tldOk = false
for _, allowed in ipairs(Config.Domain.allowedTlds) do
if tld == allowed then tldOk = true break end
end
if not tldOk then
return nil, ('Endung nicht erlaubt. Moeglich: %s')
:format(table.concat(Config.Domain.allowedTlds, ', '))
end
if not allowReserved then
for _, reserved in ipairs(Config.Domain.reserved) do
if name == reserved then return nil, 'dieser Name ist reserviert' end
end
end
return domain, nil
end
-- ── Darf dieser Spieler ohne Anmeldung fuer seinen Job registrieren? ─────────
-- Gibt (ownerType, ownerId, nil) oder (nil, nil, fehlertext) zurueck.
--
-- Der Normalfall ist ausgeschaltet: Domaenen vergibt der Anbieter ueber die
-- Verwaltung. Siehe Config.SelfRegistration.
function D.CanRegister(src)
if not Config.SelfRegistration.enabled then
return nil, nil, 'Domaenen vergibt die Administration von Liveinvader'
end
local job, grade = FW.GetJob(src)
if not job then return nil, nil, 'kein Job' end
if Config.Registration.blockedJobs[job] then
return nil, nil, 'mit diesem Job nicht moeglich'
end
local allowed = Config.Registration.allowedJobs
if allowed and next(allowed) ~= nil then
local found = false
for _, j in ipairs(allowed) do
if j == job then found = true break end
end
if not found then return nil, nil, 'dieser Job ist nicht freigeschaltet' end
end
if grade < (Config.Registration.minGrade or 0) then
return nil, nil, ('Dienstgrad zu niedrig (mindestens %d)')
:format(Config.Registration.minGrade or 0)
end
local count = DB.CountSitesForOwner('job', job)
if count >= (Config.Registration.maxDomainsPerJob or 1) then
return nil, nil, ('Grenze erreicht: %d Domaenen je Job'):format(count)
end
return 'job', job, nil
end
-- ── Bearbeitungsrecht ───────────────────────────────────────────────────────
-- Ausschliesslich ueber die Anmeldung: wer mit einem Zugang dieser Domaene
-- angemeldet ist, darf ihre Seiten pflegen. Job und Serverrechte spielen keine
-- Rolle mehr wer den Zugang kennt, verwaltet die Seite, unabhaengig davon,
-- welchen Charakter er gerade spielt.
function D.CanEdit(src, site)
if not site then return false end
return ICWeb.Auth.CanEdit(src, site.domain)
end
-- ── Mail-Domaene sicherstellen ──────────────────────────────────────────────
-- Gibt (true, nil) oder (false, fehlertext) zurueck.
--
-- Laeuft ic-mail nicht, gilt das als Erfolg: die Webseite funktioniert
-- eigenstaendig, es gibt dann nur keine Mailadressen darauf.
function D.EnsureRealm(domain, title, ownerId)
if GetResourceState('ic-mail') ~= 'started' then return true, nil end
local existing = exports['ic-mail']:GetRealmByName(domain)
if existing then
-- System- und Buergerdomaenen gehoeren niemandem und haben deshalb
-- owner_id = NULL. Ohne diese Pruefung wuerde die Besitzpruefung unten
-- durchwinken, und eine Firmenwebseite haenge plötzlich an mail.ls
-- also an der Domaene saemtlicher persoenlicher Postfaecher.
if ICWeb.Bool(existing.is_default)
or existing.type == 'system' or existing.type == 'citizen' then
return false, 'diese Domaene gehoert zum Mailsystem und ist nicht vergebbar'
end
-- Domaene gibt es schon fuer Mail. Nur uebernehmen, wenn sie demselben
-- Besitzer gehoert sonst wuerde die Webseite die Mailadressen einer
-- fremden Firma kapern.
if existing.owner_id and existing.owner_id ~= ownerId then
return false, 'diese Domaene gehoert bereits einer anderen Stelle'
end
return true, nil
end
local ok, createErr = exports['ic-mail']:CreateRealm({
realm = domain,
type = 'company',
display_name = title or domain,
owner_id = ownerId,
})
if not ok then
return false, ('Domaene konnte nicht angelegt werden: %s'):format(tostring(createErr))
end
return true, nil
end
-- ── Registrieren ────────────────────────────────────────────────────────────
-- Gibt (site, nil) oder (nil, fehlertext) zurueck.
function D.Register(src, rawDomain, title)
local ownerType, ownerId, err = D.CanRegister(src)
if not ownerType then return nil, err end
local domain, nameErr = D.CheckName(rawDomain)
if not domain then return nil, nameErr end
if DB.GetSiteByDomain(domain) then
return nil, 'diese Domaene ist bereits vergeben'
end
local realmOk, realmErr = D.EnsureRealm(domain, title, ownerId)
if not realmOk then return nil, realmErr end
local site = D.CreateSiteWithHome(domain, title, ownerType, ownerId, FW.GetIdentifier(src))
if not site then return nil, 'Speichern fehlgeschlagen' end
-- Bei der Selbstanmeldung gibt es niemanden, der einen Zugang einrichten
-- koennte. Also wird er hier erzeugt und einmalig angezeigt.
local username = 'admin@' .. domain
local password = D.RandomPassword()
local accountId = ICWeb.Auth.CreateAccount({
username = username,
domain = domain,
role = 'admin',
display_name = title or domain,
password = password,
must_change = true,
created_by = 'selfservice',
})
ICWeb.Log.Write(src, 'domain.registered', { domain = domain, owner = ownerId })
return site, nil, accountId and { username = username, password = password } or nil
end
-- Domaene, Startseite und Rueckgabe in einem. Beide Anlagewege gehen hier durch,
-- damit eine neue Domaene nie ohne Startseite dasteht.
function D.CreateSiteWithHome(domain, title, ownerType, ownerId, createdBy)
local siteId = DB.CreateSite({
domain = domain,
title = tostring(title or domain):sub(1, 120),
theme = Config.DefaultTheme,
owner_type = ownerType or 'job',
owner_id = ownerId,
published = false,
created_by = createdBy,
})
if not siteId then return nil end
DB.SavePage(siteId, 'home', title or domain, json.encode({
{ type = 'heading', text = title or domain, level = 1 },
{ type = 'text', text = 'Diese Seite wurde noch nicht bearbeitet.' },
}), 0)
return DB.GetSiteById(siteId)
end
-- Aussprechbares Startpasswort. Es muss beim ersten Anmelden geaendert werden,
-- deshalb zaehlt Merkbarkeit hier mehr als Laenge.
function D.RandomPassword()
local words = { 'anker', 'birke', 'delta', 'falke', 'gipfel', 'hafen',
'insel', 'kompass', 'lotse', 'nordwind', 'orkan', 'rebe' }
return words[math.random(#words)] .. tostring(math.random(100, 999))
end