389 lines
9.5 KiB
Lua
389 lines
9.5 KiB
Lua
local S = minetest.get_translator("accountmgr")
|
|
local function FS(...)
|
|
return minetest.formspec_escape(S(...))
|
|
end
|
|
|
|
|
|
local bulk_dialog = {}
|
|
|
|
|
|
function bulk_dialog.build(context)
|
|
local selected_user = context.selected_user or {}
|
|
context.selected_user = selected_user
|
|
|
|
-- Build cells
|
|
local selected_user_idx = -1
|
|
context.users = context.users or {}
|
|
local cells = { "#aaa", FS("Name"), FS("Privs"), FS("Notes") }
|
|
for i, user in ipairs(context.users) do
|
|
cells[#cells + 1] = "#fff"
|
|
cells[#cells + 1] = minetest.formspec_escape(user.name)
|
|
cells[#cells + 1] = minetest.formspec_escape(minetest.privs_to_string(user.privs))
|
|
cells[#cells + 1] = minetest.formspec_escape(user.notes or "")
|
|
|
|
if user.name == selected_user.name then
|
|
selected_user_idx = i + 1
|
|
end
|
|
end
|
|
|
|
-- Build file list
|
|
local files = minetest.get_dir_list(minetest.get_worldpath(), false)
|
|
local selected_file_idx = 0
|
|
local i = 1
|
|
for _, file in ipairs(files) do
|
|
if file and file:sub(-4):lower() == ".csv" and file:sub(1, 10) ~= "new-users-" then
|
|
files[i] = minetest.formspec_escape(file)
|
|
if file == context.selected_file then
|
|
selected_file_idx = i
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
for j=i, #files do
|
|
files[j] = nil
|
|
end
|
|
context.files = files
|
|
|
|
local fs = {
|
|
"formspec_version[4]",
|
|
"size[16,12]",
|
|
|
|
"tablecolumns[color;text;text;text]",
|
|
"table[0.375,0.375;7.25,11.25;users;", table.concat(cells, ","), ";",
|
|
tostring(selected_user_idx), "]",
|
|
|
|
"container[8.1875,0.375]",
|
|
}
|
|
|
|
if #files > 0 then
|
|
table.insert_all(fs, {
|
|
"label[0,0.25;", FS("Import from CSV"), "]",
|
|
"textlist[0,0.5;7.25,2;import_files;", table.concat(files, ","), ";]",
|
|
"button[0,2.75;3,0.8;refresh;", FS("Refresh"), "]",
|
|
})
|
|
if selected_file_idx ~= 0 then
|
|
table.insert_all(fs, {
|
|
"button[4.25,2.75;3,0.8;import;", FS("Import"), "]",
|
|
})
|
|
else
|
|
table.insert_all(fs, {
|
|
"box[4.25,2.75;3,0.8;#222]",
|
|
"style[import_dis;border=false,textcolor=grey]",
|
|
"button[4.25,2.75;3,0.8;import_dis;", FS("Import"), "]",
|
|
})
|
|
end
|
|
else
|
|
local help_info = minetest.formspec_escape(
|
|
S("Place a .csv file in the world directory (@1) to import users", minetest.get_worldpath()) ..
|
|
"\n\n" ..
|
|
S("This can be a list of usernames, one per line. It can also include privs.\nFull example:") ..
|
|
"\n\n" ..
|
|
"player1\nplayer2, fly fast\nplayer3, fly\nplayer3,")
|
|
table.insert_all(fs, {
|
|
"label[0,0.25;", FS("Import from CSV"), "]",
|
|
"box[0,0.5;7.25,2;#1e1e1e]",
|
|
"textarea[0.1,0.6;7.05,1.8;;;", help_info, "]",
|
|
"button[0,2.75;3,0.8;refresh;", FS("Refresh"), "]",
|
|
"box[4.25,2.75;3,0.8;#222]",
|
|
"style[import_dis;border=false,textcolor=grey]",
|
|
"button[4.25,2.75;3,0.8;import_dis;", FS("Import"), "]",
|
|
})
|
|
end
|
|
|
|
table.insert_all(fs, {
|
|
"container_end[]",
|
|
|
|
"box[8.005,4.3625;7.625,0.03;#111f]",
|
|
|
|
"container[8.1875,4.8]",
|
|
-- "box[0,0;7.25,4;#111]",
|
|
"label[0,0.25;", FS("Create / Update"), "]",
|
|
"field[0,1;3.625,0.8;user_name;", FS("Name"), ";", selected_user.name or "", "]",
|
|
"field[0,2.3;7.25,0.8;user_privs;", FS("Privileges"), ";",
|
|
minetest.privs_to_string(selected_user.privs or {}, ", "), "]",
|
|
"textarea[0,3.6;7.25,1.6;user_notes;", FS("Notes"), ";", selected_user.notes or "", "]",
|
|
"button[5.25,0;2,0.8;user_save;", FS("Save"), "]",
|
|
"button[3,0;2,0.8;user_delete;", FS("Delete"), "]",
|
|
"container_end[]",
|
|
|
|
"box[8.005,10.4125;7.625,0.03;#111f]",
|
|
|
|
"container[8.1875,10.825]",
|
|
"textarea[0,0;4.1875,0.8;;;", minetest.formspec_escape(context.message or ""), "]",
|
|
"button[4.4375,0;3,0.8;create;", FS("Create Accounts"), "]",
|
|
"container_end[]",
|
|
})
|
|
|
|
return table.concat(fs, "")
|
|
end
|
|
|
|
|
|
local function parse_csv(text)
|
|
local users = {}
|
|
|
|
local lines = text:split("\n")
|
|
for _, line in ipairs(lines) do
|
|
local parts = line:trim():split(",")
|
|
if parts[1] ~= "" then
|
|
local user = {
|
|
name = parts[1]:trim():gsub("[^A-Za-z0-9_-]", "_"),
|
|
privs = {},
|
|
notes = parts[3] and parts[3]:trim() or "",
|
|
}
|
|
|
|
if parts[2] then
|
|
user.privs = minetest.string_to_privs(parts[2], " ")
|
|
end
|
|
|
|
users[#users + 1] = user
|
|
end
|
|
end
|
|
|
|
return users
|
|
end
|
|
|
|
|
|
local function update_user(users, user)
|
|
for i, existing in ipairs(users) do
|
|
if existing.name == user.name then
|
|
users[i] = user
|
|
return
|
|
end
|
|
end
|
|
|
|
table.insert(users, user)
|
|
end
|
|
|
|
|
|
local password_charset = {}
|
|
-- a-zA-Z0-9
|
|
for i = 48, 57 do table.insert(password_charset, string.char(i)) end
|
|
for i = 65, 90 do table.insert(password_charset, string.char(i)) end
|
|
for i = 97, 122 do table.insert(password_charset, string.char(i)) end
|
|
table.insert_all(password_charset, { "_", "-", "!" })
|
|
|
|
function generate_password(length)
|
|
local ret = ""
|
|
for i=1, length do
|
|
ret = ret .. password_charset[math.random(1, #password_charset)]
|
|
end
|
|
return ret
|
|
end
|
|
|
|
|
|
local function write_reports(player, users, passwords)
|
|
local filename = "new-users-" .. os.date("!%Y-%m-%d-%T")
|
|
local filepath = minetest.get_worldpath() .. "/" .. filename
|
|
|
|
minetest.log("action", ("Exported account creation reports to %s and %s"):format(
|
|
filename .. ".csv", filename .. ".html"))
|
|
|
|
-- CSV
|
|
do
|
|
local file = io.open(filepath .. ".csv", "w")
|
|
file:write("Name, Password, Notes", "\n")
|
|
for _, user in pairs(users) do
|
|
file:write(("%s, %s, %s\n"):format(
|
|
user.name, passwords[user.name], user.notes or ""))
|
|
end
|
|
file:close()
|
|
end
|
|
|
|
-- HTML
|
|
do
|
|
local file = io.open(filepath .. ".html", "w")
|
|
|
|
local rows = ""
|
|
for _, user in pairs(users) do
|
|
rows = rows .. ("<tr><td>%s</td><td>%s</td><td>%s</td></tr>\n"):format(
|
|
user.name, passwords[user.name], user.notes or "")
|
|
end
|
|
|
|
local locale = minetest.get_player_information(player:get_player_name()).lang_code
|
|
local title = minetest.get_translated_string(locale, S("Minetest Accounts"))
|
|
|
|
file:write(([[
|
|
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<title>%s</title>
|
|
<style>
|
|
body {
|
|
font-family: "Arial", sans-serif;
|
|
padding: 0.5rem;
|
|
}
|
|
h1 {
|
|
margin-top: 0;
|
|
}
|
|
table {
|
|
width: 100%%;
|
|
border-collapse: collapse;
|
|
}
|
|
table td, table th {
|
|
border: 1px solid #ddd;
|
|
padding: 8px;
|
|
}
|
|
table tr:nth-child(even) {
|
|
background-color: #f2f2f2;
|
|
}
|
|
table th {
|
|
padding: 12px 8px;
|
|
text-align: left;
|
|
background-color: #04AA6D;
|
|
color: white;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>%s</h1>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Name</th><th>Password</th><th>Notes</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
%s
|
|
</tbody>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
]]):format(title, title, rows), "\n")
|
|
file:close()
|
|
end
|
|
|
|
return filename
|
|
end
|
|
|
|
|
|
function bulk_dialog.on_submit(player, context, fields)
|
|
if not minetest.check_player_privs(player, { server = true }) then
|
|
return
|
|
end
|
|
|
|
context.message = ""
|
|
|
|
if fields.import_files then
|
|
local e = minetest.explode_textlist_event(fields.import_files)
|
|
context.selected_file = context.files and context.files[e.index] or context.selected_file
|
|
if e.type == "DCL" then
|
|
fields.import = "true"
|
|
else
|
|
return true
|
|
end
|
|
end
|
|
|
|
if fields.users then
|
|
local e = minetest.explode_table_event(fields.users)
|
|
if e.row > 1 then
|
|
local user = context.users[e.row - 1]
|
|
context.selected_user = table.copy(user)
|
|
end
|
|
return true
|
|
end
|
|
|
|
if fields.user_name or fields.user_privs or fields.user_notes then
|
|
context.selected_user.name = fields.user_name
|
|
context.selected_user.privs = minetest.string_to_privs(fields.user_privs, ",")
|
|
context.selected_user.notes = fields.user_notes
|
|
end
|
|
|
|
if fields.user_save then
|
|
update_user(context.users, table.copy(context.selected_user))
|
|
return true
|
|
end
|
|
|
|
if fields.user_delete then
|
|
for i, user in ipairs(context.users) do
|
|
if user.name == context.selected_user.name then
|
|
table.remove(context.users, i)
|
|
context.selected_user = {}
|
|
break
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
if fields.import and context.selected_file then
|
|
context.users = context.users or {}
|
|
local path = minetest.get_worldpath() .. "/" .. context.selected_file
|
|
|
|
local file = io.open(path, "r")
|
|
if file then
|
|
local users = parse_csv(file:read("*all"))
|
|
for _, user in ipairs(users) do
|
|
update_user(context.users, user)
|
|
end
|
|
|
|
file:close()
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
if fields.create then
|
|
local passwords = {}
|
|
|
|
local handler = minetest.get_auth_handler()
|
|
for _, user in ipairs(context.users) do
|
|
passwords[user.name] = generate_password(16)
|
|
|
|
local hash = minetest.get_password_hash(user.name, passwords[user.name])
|
|
if handler.get_auth(user.name) then
|
|
handler.set_password(user.name, hash)
|
|
else
|
|
handler.create_auth(user.name, hash)
|
|
end
|
|
|
|
if next(user.privs) then
|
|
handler.set_privileges(user.name, user.privs)
|
|
end
|
|
end
|
|
|
|
local filename = write_reports(player, context.users, passwords)
|
|
context.message = S("Exported to world dir as @1", filename)
|
|
|
|
return true
|
|
end
|
|
|
|
if fields.refresh then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
|
|
local contexts = {}
|
|
minetest.register_on_leaveplayer(function(player)
|
|
contexts[player:get_player_name()] = nil
|
|
end)
|
|
|
|
|
|
function bulk_dialog.show(name)
|
|
local context = contexts[name] or {}
|
|
contexts[name] = context
|
|
|
|
local fs = bulk_dialog.build(context)
|
|
minetest.show_formspec(name, "accountmgr:create", fs)
|
|
end
|
|
|
|
|
|
minetest.register_on_player_receive_fields(function(player, formname, fields)
|
|
if formname ~= "accountmgr:create" then
|
|
return false
|
|
end
|
|
|
|
local name = player:get_player_name()
|
|
local context = contexts[name] or {}
|
|
contexts[name] = context
|
|
|
|
if bulk_dialog.on_submit(player, context, fields) then
|
|
local fs = bulk_dialog.build(context)
|
|
minetest.show_formspec(name, "accountmgr:create", fs)
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end)
|
|
|
|
return bulk_dialog
|