local digest = require 'openssl.digest' local rand = require 'openssl.rand' local pbkdf2 = require 'pbkdf2' local json = require 'json' local db = require 'db' local M = {} M.__index = M local function to_text(bytes) return (tostring(bytes):gsub(".", function(b) return ("%02x"):format(b:byte()) end)) end function M.uid_of(txn, email) local emails = txn:open("emails", true) return emails[email] end function M.lookup_username(txn, username) local usernames = txn:open("usernames", true) return usernames[username] end local function account(txn, uid) local udb = txn:open("users", true) local user = setmetatable({txn = txn, uid = uid}, M) return user end function M.get_user(txn, uid) local user = account(txn, uid) if user:get "exists" then return user end end function M.create_user(txn, email, password, username) if M.uid_of(txn, email) then return end local uid = to_text(rand.bytes(8)) local user = account(txn, uid) assert(not user:get "exists") user:set("exists", "exists") user:set_email(email) user:set_password(password) user:set_username(username) local tokens = txn:open("tokens", true) tokens[uid] = uid return user end function M.user_from_email(txn, email) local uid = M.uid_of(txn, email) if uid then return M.get_user(txn, uid) end end function M:set(property, value) local users = self.txn:open("users", true) users[self.uid.."."..property] = value return value end function M:get(property) local users = self.txn:open("users", true) return users[self.uid.."."..property] end function M:set_password(password) local params = {pbkdf2(password)} self:set("password", json.encode { hash = params[1], params = {table.unpack(params, 2)}, }) end function M:check_password(password) local pass = json.decode(self:get "password") local hash = pbkdf2(password, table.unpack(pass.params)) return hash == pass.hash end function M:set_email(email) local uid = M.uid_of(self.txn, email) assert(not uid or uid == self.uid) local emails = self.txn:open("emails", true) if self:get "email" then emails[self:get "email"] = nil end emails[email] = self.uid self:set("email", email) end function M:set_username(username) local uid = M.lookup_username(self.txn, username) assert(not uid or uid == self.uid) local usernames = self.txn:open("usernames", true) if self:get "username" then usernames[self:get "username"] = nil end usernames[username] = self.uid self:set("username", username) end function M:issue_token(service) local tokens = self.txn:open("tokens", true) local token = to_text(rand.bytes(8)) tokens[self.uid.."."..token] = service return token end function M:check_token(token) local tokens = self.txn:open("tokens", true) if tokens[self.uid.."."..token] then return tokens[self.uid.."."..token] end end function M:revoke_token(token) local tokens = self.txn:open("tokens", true) tokens[self.uid.."."..token] = nil end function M:revoke_tokens() local tokens = self.txn:open("tokens", true) local to_revoke = {} for token in db.next, tokens, self.uid do if not token:match("^"..self.uid..".") then break end table.insert(to_revoke, token) end for _, v in ipairs(to_revoke) do tokens[v] = nil end end return M