ic-web/server/manage.lua
Bjoern Flessing a2ce3a69fd 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.
2026-08-09 11:50:53 +00:00

329 lines
13 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ─────────────────────────────────────────────────────────────────────────────
-- ic-web Verwaltung
--
-- Zwei Ebenen, beide ueber die Anmeldung:
--
-- superadmin legt Domaenen an, setzt je Domaene einen Admin ein, sperrt
-- und loescht. Sieht alles.
-- admin verwaltet die Zugaenge seiner Domaene und pflegt ihre Seiten.
--
-- Ein Zugang ist zugleich ein Postfach: wer hier einen Zugang anlegt, legt
-- damit die Mailadresse mit an.
-- ─────────────────────────────────────────────────────────────────────────────
ICWeb = ICWeb or {}
ICWeb.Manage = {}
local M = ICWeb.Manage
local DB = ICWeb.DB
local FW = ICWeb.FW
local D = ICWeb.Domains
local A = ICWeb.Auth
-- Was der Client ueber einen Zugang erfahren darf.
local function publicAccount(row)
return {
id = row.id,
username = row.username,
domain = row.domain,
role = row.role,
display_name = row.display_name,
active = ICWeb.Bool(row.active),
must_change = ICWeb.Bool(row.must_change),
last_login = row.last_login,
created_at = row.created_at,
}
end
-- ── Zugaenge einer Domaene ──────────────────────────────────────────────────
function M.ListAccounts(src, domain)
domain = tostring(domain or ''):lower()
if not A.CanManageAccounts(src, domain) then return nil, 'Keine Berechtigung.' end
local out = {}
for _, row in ipairs(DB.GetAccountsForDomain(domain)) do
out[#out + 1] = publicAccount(row)
end
return out, nil
end
-- Gibt (zugang, nil) oder (nil, fehlertext) zurueck.
--
-- Das Passwort wird vom Anleger vergeben und danach weitergegeben. Es kommt
-- deshalb einmal im Klartext zurueck an den Bildschirm des Anlegers, aber nie
-- wieder aus der Datenbank heraus.
function M.CreateAccount(src, domain, username, password, role, displayName)
domain = tostring(domain or ''):lower()
if not A.CanManageAccounts(src, domain) then return nil, 'Keine Berechtigung.' end
local site = DB.GetSiteByDomain(domain)
if not site then return nil, 'Domäne nicht gefunden.' end
-- Nur der Anbieter darf weitere Anbieterzugaenge oder Domaenenadmins
-- ausserhalb der eigenen Domaene anlegen.
if role == 'superadmin' and not A.IsSuperadmin(src) then
return nil, 'Keine Berechtigung für diese Rolle.'
end
if role ~= 'admin' and role ~= 'editor' and role ~= 'superadmin' then
return nil, 'Unbekannte Rolle.'
end
if DB.CountAccountsForDomain(domain) >= (Config.MaxAccountsPerDomain or 15) then
return nil, ('Höchstens %d Zugänge je Domäne.'):format(Config.MaxAccountsPerDomain or 15)
end
local session = A.Get(src)
local id, err = A.CreateAccount({
username = username,
domain = domain,
role = role,
display_name = displayName,
password = password,
must_change = true,
created_by = session and session.username or 'system',
})
if not id then return nil, err end
ICWeb.Log.Write(src, 'account.created',
{ domain = domain, username = tostring(username):lower(), role = role })
return publicAccount(DB.GetAccountById(id)), nil
end
function M.SetAccountPassword(src, accountId, password)
local account = DB.GetAccountById(tonumber(accountId) or -1)
if not account then return false, 'Zugang nicht gefunden.' end
-- Den eigenen Zugang darf jeder aendern, fremde nur die Verwaltung.
local session = A.Get(src)
local own = session ~= nil and session.username == account.username
if not own and not A.CanManageAccounts(src, account.domain) then
return false, 'Keine Berechtigung.'
end
-- Der Anbieterzugang laesst sich nur von ihm selbst oder einem anderen
-- Anbieterzugang aendern.
if account.role == 'superadmin' and not own and not A.IsSuperadmin(src) then
return false, 'Keine Berechtigung.'
end
local ok, err = A.SetPassword(account.id, password)
if not ok then return false, err end
if own then A.ClearMustChange(src) end
ICWeb.Log.Write(src, 'account.password.changed', { username = account.username })
return true, nil
end
function M.SetAccountActive(src, accountId, active)
local account = DB.GetAccountById(tonumber(accountId) or -1)
if not account then return false, 'Zugang nicht gefunden.' end
if not A.CanManageAccounts(src, account.domain) then return false, 'Keine Berechtigung.' end
if account.role == 'superadmin' and not A.IsSuperadmin(src) then
return false, 'Keine Berechtigung.'
end
local session = A.Get(src)
if session and session.username == account.username then
return false, 'Den eigenen Zugang kannst du nicht sperren.'
end
DB.SetAccountActive(account.id, active == true)
ICWeb.Log.Write(src, active and 'account.enabled' or 'account.disabled',
{ username = account.username })
return true, nil
end
function M.SetAccountRole(src, accountId, role)
local account = DB.GetAccountById(tonumber(accountId) or -1)
if not account then return false, 'Zugang nicht gefunden.' end
if not A.CanManageAccounts(src, account.domain) then return false, 'Keine Berechtigung.' end
if role ~= 'admin' and role ~= 'editor' then return false, 'Unbekannte Rolle.' end
if account.role == 'superadmin' then return false, 'Der Anbieterzugang bleibt, wie er ist.' end
local session = A.Get(src)
if session and session.username == account.username then
return false, 'Die eigene Rolle kannst du nicht ändern.'
end
DB.SetAccountRole(account.id, role)
ICWeb.Log.Write(src, 'account.role.changed', { username = account.username, role = role })
return true, nil
end
-- Loeschen entfernt den Zugang, nicht das Postfach: dort liegen Nachrichten.
function M.DeleteAccount(src, accountId)
local account = DB.GetAccountById(tonumber(accountId) or -1)
if not account then return false, 'Zugang nicht gefunden.' end
if not A.CanManageAccounts(src, account.domain) then return false, 'Keine Berechtigung.' end
if account.role == 'superadmin' then return false, 'Der Anbieterzugang lässt sich nicht löschen.' end
local session = A.Get(src)
if session and session.username == account.username then
return false, 'Den eigenen Zugang kannst du nicht löschen.'
end
DB.DeleteAccount(account.id)
ICWeb.Log.Write(src, 'account.deleted', { username = account.username }, 'warn')
return true, nil
end
-- ── Anbieterebene ───────────────────────────────────────────────────────────
function M.AdminListSites(src)
if not A.IsSuperadmin(src) then return nil, 'Keine Berechtigung.' end
local out = {}
for _, s in ipairs(DB.GetAllSites()) do
out[#out + 1] = {
domain = s.domain,
title = s.title,
owner_type = s.owner_type,
owner_id = s.owner_id,
published = ICWeb.Bool(s.published),
blocked = ICWeb.Bool(s.blocked),
pages = DB.CountPages(s.id),
accounts = DB.CountAccountsForDomain(s.domain),
created_at = s.created_at,
}
end
return out, nil
end
-- Domaene anlegen und gleich den ersten Zugang dafuer einrichten. Ohne diesen
-- Zugang koennte niemand die neue Seite pflegen.
--
-- Gibt (site, nil, zugangsdaten) oder (nil, fehlertext) zurueck.
function M.AdminRegister(src, domain, title, ownerId, adminUser, adminPassword)
if not A.IsSuperadmin(src) then return nil, 'Keine Berechtigung.' end
-- Der Anbieter darf auch Behoerdennamen vergeben dafuer ist er da.
local name, err = D.CheckName(domain, true)
if not name then return nil, err end
ownerId = tostring(ownerId or ''):lower()
if ownerId == '' then return nil, 'Kein Besitzer (Job) angegeben.' end
if DB.GetSiteByDomain(name) then return nil, 'Diese Domäne ist bereits vergeben.' end
-- Zugangsdaten zuerst pruefen: eine Domaene ohne Zugang waere unbrauchbar,
-- und ein Rueckbau nach dem Anlegen ist fehleranfaelliger als eine
-- Pruefung davor.
local user = tostring(adminUser or 'admin'):lower():gsub('[^a-z0-9%.%-_]', '')
if user == '' then user = 'admin' end
local username = user .. '@' .. name
local password = adminPassword
local generated = false
if type(password) ~= 'string' or password == '' then
password = D.RandomPassword()
generated = true
end
local pwErr = A.CheckPassword(password)
if pwErr then return nil, pwErr end
local realmOk, realmErr = D.EnsureRealm(name, title, ownerId)
if not realmOk then return nil, realmErr end
local site = D.CreateSiteWithHome(name, title, 'job', ownerId,
(A.Get(src) or {}).username or 'system')
if not site then return nil, 'Speichern fehlgeschlagen.' end
local accountId, accErr = A.CreateAccount({
username = username,
domain = name,
role = 'admin',
display_name = title or name,
password = password,
must_change = true,
created_by = (A.Get(src) or {}).username or 'system',
})
if not accountId then
-- Ohne Zugang ist die Domaene nicht pflegbar; lieber nichts stehen lassen.
DB.DeleteSite(site.id)
return nil, accErr
end
ICWeb.Log.Write(src, 'domain.registered', { domain = name, owner = ownerId }, 'warn')
return site, nil, { username = username, password = password, generated = generated }
end
function M.AdminSetBlocked(src, domain, blocked)
if not A.IsSuperadmin(src) then return false, 'Keine Berechtigung.' end
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
DB.SetBlocked(site.id, blocked == true)
ICWeb.Log.Write(src, blocked and 'site.blocked' or 'site.unblocked',
{ domain = site.domain }, 'warn')
return true, nil
end
-- Besitzer wechseln, etwa wenn eine Firma verkauft wird.
function M.AdminSetOwner(src, domain, ownerId)
if not A.IsSuperadmin(src) then return false, 'Keine Berechtigung.' end
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
ownerId = tostring(ownerId or ''):lower()
if ownerId == '' then return false, 'Kein Besitzer angegeben.' end
DB.SetOwner(site.id, 'job', ownerId)
ICWeb.Log.Write(src, 'site.owner.changed',
{ domain = site.domain, from = site.owner_id, to = ownerId }, 'warn')
return true, nil
end
-- Loeschen entfernt Seiten und Meldungen ueber ON DELETE CASCADE sowie die
-- Zugaenge dieser Domaene. Die Mail-Domaene und die Postfaecher bleiben
-- bestehen: dort liegen Nachrichten, die eine geloeschte Webseite nichts angehen.
function M.AdminDeleteSite(src, domain)
if not A.IsSuperadmin(src) then return false, 'Keine Berechtigung.' end
local site = DB.GetSiteByDomain(tostring(domain or ''):lower())
if not site then return false, 'Domäne nicht gefunden.' end
for _, acc in ipairs(DB.GetAccountsForDomain(site.domain)) do
DB.DeleteAccount(acc.id)
end
DB.DeleteSite(site.id)
ICWeb.Log.Write(src, 'site.deleted', { domain = site.domain }, 'warn')
return true, nil
end
function M.AdminListReports(src)
if not A.IsSuperadmin(src) then return nil, 'Keine Berechtigung.' end
return DB.GetOpenReports(), nil
end
function M.AdminSetReportStatus(src, id, status)
if not A.IsSuperadmin(src) then return false, 'Keine Berechtigung.' end
if status ~= 'reviewed' and status ~= 'dismissed' and status ~= 'open' then
return false, 'Unbekannter Status.'
end
DB.SetReportStatus(tonumber(id) or -1, status)
return true, nil
end
-- Jobliste fuer das Besitzerfeld in der Verwaltung.
function M.AdminListJobs(src)
if not A.IsSuperadmin(src) then return nil, 'Keine Berechtigung.' end
return FW.GetJobs(), nil
end
-- Alle Zugaenge quer ueber alle Domaenen.
function M.AdminListAccounts(src)
if not A.IsSuperadmin(src) then return nil, 'Keine Berechtigung.' end
local out = {}
for _, row in ipairs(DB.GetAllAccounts()) do
out[#out + 1] = publicAccount(row)
end
return out, nil
end