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.
235 lines
9.6 KiB
Lua
235 lines
9.6 KiB
Lua
-- ─────────────────────────────────────────────────────────────────────────────
|
||
-- 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
|