Files
readest/apps/readest.koplugin/readest_syncauth.lua
T

221 lines
7.4 KiB
Lua

local Device = require("device")
local InfoMessage = require("ui/widget/infomessage")
local MultiInputDialog = require("ui/widget/multiinputdialog")
local NetworkMgr = require("ui/network/manager")
local UIManager = require("ui/uimanager")
local logger = require("logger")
local util = require("util")
local T = require("ffi/util").template
local _ = require("readest_i18n")
local SyncAuth = {}
function SyncAuth:needsLogin(settings)
return not settings.access_token or not settings.expires_at
or settings.expires_at < os.time() + 60
end
function SyncAuth:tryRefreshToken(settings, path)
if settings.refresh_token and settings.expires_at
and settings.expires_at < os.time() + settings.expires_in / 2 then
local client = self:getSupabaseAuthClient(settings, path)
client:refresh_token(settings.refresh_token, function(success, response)
if success then
settings.access_token = response.access_token
settings.refresh_token = response.refresh_token
settings.expires_at = response.expires_at
settings.expires_in = response.expires_in
G_reader_settings:saveSetting("readest_sync", settings)
else
logger.err("ReadestSync: Token refresh failed:", response or "Unknown error")
end
end)
end
end
-- Block-style wrapper around tryRefreshToken: runs the refresh (if needed)
-- and invokes `callback(ok, err)` after the new token is committed to
-- settings, OR immediately with ok=true if the token is still fresh.
--
-- Codex round 1 finding 14: ensureClient() in main.lua kicks off a refresh
-- and immediately builds a Spore client with whatever token was already in
-- settings — racy. New library API calls (pullBooks, getDownloadUrl) MUST go
-- through this wrapper so the request body never carries a stale Bearer
-- header.
function SyncAuth:withFreshToken(settings, path, callback)
-- Token still has > 50% TTL remaining: nothing to do.
if not settings.refresh_token or not settings.expires_at
or settings.expires_at >= os.time() + (settings.expires_in or 0) / 2 then
if callback then callback(true) end
return
end
local client = self:getSupabaseAuthClient(settings, path)
if not client then
if callback then callback(false, "no auth client") end
return
end
client:refresh_token(settings.refresh_token, function(success, response)
if success then
settings.access_token = response.access_token
settings.refresh_token = response.refresh_token
settings.expires_at = response.expires_at
settings.expires_in = response.expires_in
G_reader_settings:saveSetting("readest_sync", settings)
if callback then callback(true) end
else
logger.err("ReadestSync: Token refresh failed:", response or "Unknown error")
if callback then callback(false, response and response.msg or "refresh failed") end
end
end)
end
function SyncAuth:getSupabaseAuthClient(settings, path)
if not settings.supabase_url or not settings.supabase_anon_key then
return nil
end
local SupabaseAuthClient = require("readest_supabaseauth")
return SupabaseAuthClient:new{
service_spec = path .. "/supabase-auth-api.json",
custom_url = settings.supabase_url .. "/auth/v1/",
api_key = settings.supabase_anon_key,
}
end
function SyncAuth:getReadestSyncClient(settings, path)
if not settings.access_token or not settings.expires_at or settings.expires_at < os.time() then
return nil
end
local ReadestSyncClient = require("readest_syncclient")
return ReadestSyncClient:new{
service_spec = path .. "/readest-sync-api.json",
access_token = settings.access_token,
}
end
function SyncAuth:login(settings, path, title, menu)
if NetworkMgr:willRerunWhenOnline(function() self:login(settings, path, title, menu) end) then
return
end
local dialog
dialog = MultiInputDialog:new{
title = title,
fields = {
{
text = settings.user_email,
hint = "email@example.com",
},
{
hint = "password",
text_type = "password",
},
},
buttons = {
{
{
text = _("Cancel"),
id = "close",
callback = function()
UIManager:close(dialog)
end,
},
{
text = _("Login"),
callback = function()
local email, password = unpack(dialog:getFields())
email = util.trim(email)
if email == "" or password == "" then
UIManager:show(InfoMessage:new{
text = _("Please enter both email and password"),
timeout = 2,
})
return
end
UIManager:close(dialog)
self:doLogin(settings, path, email, password, menu)
end,
},
},
},
}
UIManager:show(dialog)
dialog:onShowKeyboard()
end
function SyncAuth:doLogin(settings, path, email, password, menu)
local client = self:getSupabaseAuthClient(settings, path)
if not client then
UIManager:show(InfoMessage:new{
text = _("Please configure Supabase URL and API key first"),
timeout = 3,
})
return
end
UIManager:show(InfoMessage:new{
text = _("Logging in..."),
timeout = 1,
})
Device:setIgnoreInput(true)
local success, response = client:sign_in_password(email, password)
Device:setIgnoreInput(false)
if success then
settings.user_email = email
settings.user_id = response.user.id
settings.user_name = response.user.user_metadata.user_name or email
settings.access_token = response.access_token
settings.refresh_token = response.refresh_token
settings.expires_at = response.expires_at
settings.expires_in = response.expires_in
G_reader_settings:saveSetting("readest_sync", settings)
if menu then
menu:updateItems()
end
UIManager:show(InfoMessage:new{
text = _("Successfully logged in to Readest"),
timeout = 3,
})
else
UIManager:show(InfoMessage:new{
text = T(_("Login failed: %1"), response.msg or _("unknown error")),
timeout = 3,
})
end
end
function SyncAuth:logout(settings, path, menu)
if settings.access_token then
local client = self:getSupabaseAuthClient(settings, path)
if client then
client:sign_out(settings.access_token, function(success, _response)
logger.dbg("ReadestSync: Sign out result:", success)
end)
end
end
settings.access_token = nil
settings.refresh_token = nil
settings.expires_at = nil
settings.expires_in = nil
G_reader_settings:saveSetting("readest_sync", settings)
if menu then
menu:updateItems()
end
UIManager:show(InfoMessage:new{
text = _("Logged out from Readest"),
timeout = 2,
})
end
return SyncAuth