ic-web/server/database.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

235 lines
9.6 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 Datenzugriff
-- Saemtliches SQL steht hier. Der uebrige Code kennt keine Tabellennamen.
-- ─────────────────────────────────────────────────────────────────────────────
ICWeb = ICWeb or {}
ICWeb.DB = {}
local DB = ICWeb.DB
-- TINYINT(1) kommt je nach Treiberfassung als 1, als true oder als "1" an.
-- Ein Vergleich mit == 1 geht deshalb frueher oder spaeter schief und zwar
-- still: aus "aktiv" wird "gesperrt", ohne Fehlermeldung. Deshalb laeuft jede
-- Ja/Nein-Spalte aus der Datenbank durch diese Funktion.
function ICWeb.Bool(value)
if value == nil or value == false then return false end
if value == true then return true end
if type(value) == 'number' then return value ~= 0 end
if type(value) == 'string' then
return value ~= '0' and value ~= '' and value:lower() ~= 'false'
end
return true
end
-- ── Sites ───────────────────────────────────────────────────────────────────
function DB.GetSiteByDomain(domain)
return MySQL.single.await('SELECT * FROM ic_web_sites WHERE domain = ?', { domain })
end
function DB.GetSiteById(id)
return MySQL.single.await('SELECT * FROM ic_web_sites WHERE id = ?', { id })
end
function DB.GetSitesForOwner(ownerType, ownerId)
return MySQL.query.await(
'SELECT * FROM ic_web_sites WHERE owner_type = ? AND owner_id = ? ORDER BY domain',
{ ownerType, ownerId }) or {}
end
function DB.CountSitesForOwner(ownerType, ownerId)
local row = MySQL.single.await(
'SELECT COUNT(*) AS n FROM ic_web_sites WHERE owner_type = ? AND owner_id = ?',
{ ownerType, ownerId })
return (row and tonumber(row.n)) or 0
end
function DB.GetAllSites()
return MySQL.query.await('SELECT * FROM ic_web_sites ORDER BY domain') or {}
end
function DB.SetOwner(id, ownerType, ownerId)
return MySQL.update.await(
'UPDATE ic_web_sites SET owner_type = ?, owner_id = ? WHERE id = ?',
{ ownerType, ownerId, id })
end
function DB.CreateSite(data)
return MySQL.insert.await(
[[INSERT INTO ic_web_sites (domain, title, theme, owner_type, owner_id, published, created_by)
VALUES (?,?,?,?,?,?,?)]],
{ data.domain, data.title, data.theme, data.owner_type, data.owner_id,
data.published and 1 or 0, data.created_by })
end
function DB.UpdateSite(id, data)
return MySQL.update.await(
'UPDATE ic_web_sites SET title = ?, theme = ?, published = ? WHERE id = ?',
{ data.title, data.theme, data.published and 1 or 0, id })
end
function DB.SetBlocked(id, blocked)
return MySQL.update.await('UPDATE ic_web_sites SET blocked = ? WHERE id = ?',
{ blocked and 1 or 0, id })
end
function DB.DeleteSite(id)
return MySQL.update.await('DELETE FROM ic_web_sites WHERE id = ?', { id })
end
-- ── Seiten ──────────────────────────────────────────────────────────────────
function DB.GetPages(siteId)
return MySQL.query.await(
'SELECT * FROM ic_web_pages WHERE site_id = ? ORDER BY sort_order, id', { siteId }) or {}
end
function DB.GetPage(siteId, slug)
return MySQL.single.await(
'SELECT * FROM ic_web_pages WHERE site_id = ? AND slug = ?', { siteId, slug })
end
function DB.CountPages(siteId)
local row = MySQL.single.await(
'SELECT COUNT(*) AS n FROM ic_web_pages WHERE site_id = ?', { siteId })
return (row and tonumber(row.n)) or 0
end
-- Anlegen oder ueberschreiben. Der eindeutige Schluessel ist (site_id, slug).
function DB.SavePage(siteId, slug, title, blocksJson, sortOrder)
return MySQL.update.await(
[[INSERT INTO ic_web_pages (site_id, slug, title, blocks, sort_order)
VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE title = VALUES(title), blocks = VALUES(blocks),
sort_order = VALUES(sort_order)]],
{ siteId, slug, title, blocksJson, sortOrder or 0 })
end
function DB.DeletePage(siteId, slug)
return MySQL.update.await(
'DELETE FROM ic_web_pages WHERE site_id = ? AND slug = ?', { siteId, slug })
end
-- ── Zugaenge ────────────────────────────────────────────────────────────────
-- Das Passwort erscheint hier nur als Abfrageparameter. Gehasht wird in der
-- Datenbank, damit weder Klartext noch Hash in einer Lua-Variable landen.
local ACCOUNT_FIELDS =
'id, username, domain, role, display_name, active, must_change, created_by, created_at, last_login'
function DB.GetAccountByUsername(username)
return MySQL.single.await(
'SELECT ' .. ACCOUNT_FIELDS .. ' FROM ic_web_accounts WHERE username = ?', { username })
end
function DB.GetAccountById(id)
return MySQL.single.await(
'SELECT ' .. ACCOUNT_FIELDS .. ' FROM ic_web_accounts WHERE id = ?', { id })
end
function DB.GetAccountsForDomain(domain)
return MySQL.query.await(
'SELECT ' .. ACCOUNT_FIELDS .. ' FROM ic_web_accounts WHERE domain = ? ORDER BY username',
{ domain }) or {}
end
function DB.CountAccountsForDomain(domain)
local row = MySQL.single.await(
'SELECT COUNT(*) AS n FROM ic_web_accounts WHERE domain = ?', { domain })
return (row and tonumber(row.n)) or 0
end
function DB.GetAllAccounts()
return MySQL.query.await(
'SELECT ' .. ACCOUNT_FIELDS .. ' FROM ic_web_accounts ORDER BY domain, username') or {}
end
function DB.CreateAccount(data)
return MySQL.insert.await(
[[INSERT INTO ic_web_accounts
(username, domain, role, display_name, salt, password_hash, must_change, created_by)
VALUES (?,?,?,?,?, SHA2(CONCAT(?, ?), 256), ?, ?)]],
{ data.username, data.domain, data.role, data.display_name,
data.salt, data.salt, data.password,
data.must_change and 1 or 0, data.created_by })
end
-- Gibt den Zugang zurueck, wenn das Passwort stimmt, sonst nil.
function DB.CheckPassword(username, password)
return MySQL.single.await(
'SELECT ' .. ACCOUNT_FIELDS .. ' FROM ic_web_accounts'
.. ' WHERE username = ? AND password_hash = SHA2(CONCAT(salt, ?), 256)',
{ username, password })
end
function DB.SetPassword(id, salt, password)
return MySQL.update.await(
'UPDATE ic_web_accounts SET salt = ?, password_hash = SHA2(CONCAT(?, ?), 256),'
.. ' must_change = 0 WHERE id = ?',
{ salt, salt, password, id })
end
function DB.SetAccountActive(id, active)
return MySQL.update.await('UPDATE ic_web_accounts SET active = ? WHERE id = ?',
{ active and 1 or 0, id })
end
function DB.SetAccountRole(id, role)
return MySQL.update.await('UPDATE ic_web_accounts SET role = ? WHERE id = ?', { role, id })
end
function DB.DeleteAccount(id)
return MySQL.update.await('DELETE FROM ic_web_accounts WHERE id = ?', { id })
end
function DB.TouchLogin(id)
return MySQL.update.await('UPDATE ic_web_accounts SET last_login = NOW() WHERE id = ?', { id })
end
-- ── Fehlversuche ────────────────────────────────────────────────────────────
function DB.AddLoginFail(username, identifier)
return MySQL.insert.await(
'INSERT INTO ic_web_login_fails (username, identifier) VALUES (?,?)',
{ username, identifier })
end
function DB.CountLoginFails(username, withinSeconds)
local row = MySQL.single.await(
[[SELECT COUNT(*) AS n FROM ic_web_login_fails
WHERE username = ? AND created_at > DATE_SUB(NOW(), INTERVAL ? SECOND)]],
{ username, withinSeconds })
return (row and tonumber(row.n)) or 0
end
function DB.ClearLoginFails(username)
return MySQL.update.await('DELETE FROM ic_web_login_fails WHERE username = ?', { username })
end
function DB.PruneLoginFails(olderThanSeconds)
return MySQL.update.await(
'DELETE FROM ic_web_login_fails WHERE created_at < DATE_SUB(NOW(), INTERVAL ? SECOND)',
{ olderThanSeconds })
end
-- ── Meldungen ───────────────────────────────────────────────────────────────
function DB.AddReport(siteId, pageSlug, reason, reportedBy)
return MySQL.insert.await(
'INSERT INTO ic_web_reports (site_id, page_slug, reason, reported_by) VALUES (?,?,?,?)',
{ siteId, pageSlug, reason, reportedBy })
end
function DB.RecentReportBy(siteId, identifier, withinSeconds)
return MySQL.single.await(
[[SELECT id FROM ic_web_reports
WHERE site_id = ? AND reported_by = ?
AND created_at > DATE_SUB(NOW(), INTERVAL ? SECOND) LIMIT 1]],
{ siteId, identifier, withinSeconds })
end
function DB.GetOpenReports()
return MySQL.query.await(
[[SELECT r.*, s.domain FROM ic_web_reports r
JOIN ic_web_sites s ON s.id = r.site_id
WHERE r.status = 'open' ORDER BY r.created_at DESC LIMIT 100]]) or {}
end
function DB.SetReportStatus(id, status)
return MySQL.update.await('UPDATE ic_web_reports SET status = ? WHERE id = ?', { status, id })
end