util-sql-logins.lua / last modification: 2020-01-30 14:16
if not modules then modules = { } end modules ['util-sql-logins'] = {
    version   = 1.001,
    comment   = "companion to lmx-*",
    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
    copyright = "PRAGMA ADE / ConTeXt Development Team",
    license   = "see context related readme files"
}

if not utilities.sql then require("util-sql") end

local sql              = utilities.sql
local sqlexecute       = sql.execute
local sqlmakeconverter = sql.makeconverter

local format = string.format
local ostime = os.time
local formatter = string.formatter

local trace_logins  = true
local report_logins = logs.reporter("sql","logins")

local logins = sql.logins or { }
sql.logins   = logins

logins.maxnoflogins = logins.maxnoflogins or 10
logins.cooldowntime = logins.cooldowntime or 10 * 60
logins.purgetime    = logins.purgetime    or  1 * 60 * 60
logins.autopurge    = true

local function checkeddb(presets,datatable)
    return sql.usedatabase(presets,datatable or presets.datatable or "logins")
end

logins.usedb = checkeddb

local template = [[
    CREATE TABLE IF NOT EXISTS %basename% (
        `id`    int(11)     NOT NULL AUTO_INCREMENT,
        `name`  varchar(50) NOT NULL,
        `time`  int(11)     DEFAULT '0',
        `n`     int(11)     DEFAULT '0',
        `state` int(11)     DEFAULT '0',

        PRIMARY KEY                  (`id`),
        UNIQUE KEY `id_unique_index` (`id`),
        UNIQUE KEY `name_unique_key` (`name`)
    ) DEFAULT CHARSET = utf8 ;
]]

local sqlite_template = [[
    CREATE TABLE IF NOT EXISTS %basename% (
        `id`    INTEGER NOT NULL AUTO_INCREMENT,
        `name`  TEXT    NOT NULL,
        `time`  INTEGER DEFAULT '0',
        `n`     INTEGER DEFAULT '0',
        `state` INTEGER DEFAULT '0'
    ) ;
]]

function logins.createdb(presets,datatable)

    local db = checkeddb(presets,datatable)

    local data, keys = db.execute {
        template  = db.usedmethod == "sqlite" and sqlite_template or template,
        variables = {
            basename = db.basename,
        },
    }

    report_logins("datatable %a created in %a",db.name,db.base)

    return db

end

local template =[[
    DROP TABLE IF EXISTS %basename% ;
]]

function logins.deletedb(presets,datatable)

    local db = checkeddb(presets,datatable)

    local data, keys = db.execute {
        template  = template,
        variables = {
            basename = db.basename,
        },
    }

    report_logins("datatable %a removed in %a",db.name,db.base)

end

local states = {
    [0] = "unset",
    [1] = "known",
    [2] = "unknown",
}

local converter_fetch, fields_fetch = sqlmakeconverter {
    { name = "id",    type = "number" },
    { name = "name",  type = "string" },
    { name = "time",  type = "number" },
    { name = "n",     type = "number" },
    { name = "state", type = "number" }, -- faster than mapping
}

local template_fetch = format( [[
    SELECT
      %s
    FROM
        `logins`
    WHERE
        `name` = '%%[name]%%'
]], fields_fetch )

local template_insert = [[
    INSERT INTO `logins`
        ( `name`, `state`, `time`, `n`)
    VALUES
        ('%[name]%', %state%, %time%, %n%)
]]

local template_update = [[
    UPDATE
        `logins`
    SET
        `state` = %state%,
        `time` = %time%,
        `n` = %n%
    WHERE
        `name` = '%[name]%'
]]

local template_delete = [[
    DELETE FROM
        `logins`
    WHERE
        `name` = '%[name]%'
]]

local template_purge = [[
    DELETE FROM
        `logins`
    WHERE
        `time` < '%time%'
]]

-- todo: auto cleanup (when new attempt)

local cache = { } setmetatable(cache, { __mode = 'v' })

-- local function usercreate(presets)
--     sqlexecute {
--         template = template_create,
--         presets  = presets,
--     }
-- end

function logins.userunknown(db,name)
    local d = {
        name  = name,
        state = 2,
        time  = ostime(),
        n     = 0,
    }
    db.execute {
        template  = template_update,
        variables = d,
    }
    cache[name] = d
    report_logins("user %a is registered as unknown",name)
end

function logins.userknown(db,name)
    local d = {
        name  = name,
        state = 1,
        time  = ostime(),
        n     = 0,
    }
    db.execute {
        template  = template_update,
        variables = d,
    }
    cache[name] = d
    report_logins("user %a is registered as known",name)
end

function logins.userreset(db,name)
    db.execute {
        template  = template_delete,
    }
    cache[name] = nil
    report_logins("user %a is reset",name)
end

local function userpurge(db,delay)
    db.execute {
        template  = template_purge,
        variables = {
            time  = ostime() - (delay or logins.purgetime),
        }
    }
    cache = { }
    report_logins("users are purged")
end

logins.userpurge = userpurge

local function verdict(okay,...)
--     if not trace_logins then
--         -- no tracing
--     else
    if okay then
        report_logins("%s, granted",formatter(...))
    else
        report_logins("%s, blocked",formatter(...))
    end
    return okay
end

local lasttime  = 0

function logins.userpermitted(db,name)
    local currenttime = ostime()
    if logins.autopurge and (lasttime == 0 or (currenttime - lasttime > logins.purgetime)) then
        report_logins("automatic purge triggered")
        userpurge(db)
        lasttime = currenttime
    end
    local data = cache[name]
    if data then
        report_logins("user %a is cached",name)
    else
        report_logins("user %a is fetched",name)
        data = db.execute {
            template  = template_fetch,
            converter = converter_fetch,
            variables = {
                name = name,
            }
        }
    end
    if not data or not data.name then
        if not data then
            report_logins("no user data for %a",name)
        else
            report_logins("no name entry for %a",name)
        end
        local d = {
            name  = name,
            state = 0,
            time  = currenttime,
            n     = 1,
        }
        db.execute {
            template  = template_insert,
            variables = d,
        }
        cache[name] = d
        return verdict(true,"creating new entry for %a",name)
    end
    cache[name] = data[1]
    local state = data.state
    if state == 2 then -- unknown
        return verdict(false,"user %a has state %a",name,states[state])
    end
    local n = data.n
    local m = logins.maxnoflogins
    if n > m then
        local deltatime = currenttime - data.time
        local cooldowntime = logins.cooldowntime
        if deltatime < cooldowntime then
            return verdict(false,"user %a is blocked for %s seconds out of %s",name,cooldowntime-deltatime,cooldowntime)
        else
            n = 0
        end
    end
    if n == 0 then
        local d = {
            name  = name,
            state = 0,
            time  = currenttime,
            n     = 1,
        }
        db.execute {
            template  = template_update,
            variables = d,
        }
        cache[name] = d
        return verdict(true,"user %a gets a first chance",name)
    else
        local d = {
            name  = name,
            state = 0,
            time  = currenttime,
            n     = n + 1,
        }
        db.execute {
            template  = template_update,
            variables = d,
        }
        cache[name] = d
        return verdict(true,"user %a gets a new chance, %s attempts out of %s done",name,n,m)
    end
end

return logins