Initial commit, blog system works

This commit is contained in:
2026-07-16 21:08:50 -05:00
commit 3841122960
22 changed files with 5628 additions and 0 deletions

101
.lua/auth.lua Normal file
View File

@@ -0,0 +1,101 @@
-- auth.lua — password-based session authentication using argon2id.
--
-- Sessions are stored in the `sessions` SQLite table so they survive
-- Redbean's per-request fork model (each request gets a fresh process,
-- so in-memory tables cannot be shared between requests).
--
-- The admin password hash is stored in the `settings` table under the
-- key "admin_password_hash". Use setup_admin.lua to set it.
local db = require "db"
local M = {}
local SESSION_TTL = 7 * 24 * 3600 -- 7 days in seconds
-- ── Utilities ─────────────────────────────────────────────────────────
local function to_hex(s)
return (s:gsub(".", function(c) return string.format("%02x", c:byte()) end))
end
-- Generate a cryptographically random 64-char hex token (32 bytes).
local function random_token()
local okg, rnd = pcall(GetRandomBytes, 32)
if okg and rnd and #rnd == 32 then return to_hex(rnd) end
local ok, bytes = pcall(unix.getrandom, 32)
if ok and bytes and #bytes == 32 then return to_hex(bytes) end
local f = io.open("/dev/urandom", "rb")
if f then
local b = f:read(32); f:close()
if b and #b == 32 then return to_hex(b) end
end
error("no source of randomness available")
end
-- Returns the raw token if the current request carries a live DB session.
local function current_token()
local tok = GetCookie("session")
if not tok or tok == "" then return nil end
local row = db.get_session(tok)
return row and tok or nil
end
-- ── Public API ────────────────────────────────────────────────────────
--- Returns true when the current request has a valid admin session.
function M.has_session()
return current_token() ~= nil
end
--- Enforce a valid admin session.
--- Returns nil when authorised.
--- When the session is missing, sets 302 headers and returns a non-empty
--- body (" ") so that Fullmoon calls Write(), which flushes SetHeader/
--- SetStatus to the socket. (Returning "" skips Write and the headers
--- are never sent.)
function M.require_session()
if current_token() then return nil end
SetStatus(302)
SetHeader("Location", "/login")
return " "
end
--- Verify a password and, if correct, create a DB-backed session.
--- Returns: token (string), nil on success.
--- nil, error_msg on failure.
function M.login(password)
if not password or password == "" then
return nil, "Password is required."
end
local hash = db.get_setting("admin_password_hash")
if not hash or hash == "" then
return nil, "Admin account not configured. Run setup_admin.lua first."
end
local ok, result = pcall(function()
return argon2.verify(hash, password)
end)
if not ok then
Log(kLogWarn, "argon2.verify error: " .. tostring(result))
return nil, "Internal error during authentication."
end
if not result then
return nil, "Incorrect password."
end
-- Clean up stale sessions before creating a new one.
pcall(db.prune_sessions)
local tok = random_token()
db.create_session(tok, os.time() + SESSION_TTL)
return tok, nil
end
--- Invalidate a session (logout).
function M.logout(token)
if token then pcall(db.delete_session, token) end
end
return M