- Introduce `secret_tool_lookup` to retrieve passwords via `secret-tool` - Extract `login_with_username` to try Secret Service before prompting - Read `JASPER_USERNAME` env var to skip username prompt when set - Fall back to `inputsecret` prompt if Secret Service lookup fails
157 lines
4.8 KiB
Lua
157 lines
4.8 KiB
Lua
--- jasper/auth.lua
|
|
--- Token management, shared with the jasper_waybar.py Python script.
|
|
--- Token cache: $XDG_CONFIG_HOME/jasper/token.json (same path as Python)
|
|
|
|
local M = {}
|
|
|
|
local constants = require("jasper.constants")
|
|
local JASPER_URL = constants.JASPER_URL
|
|
|
|
--- @return string path to the token cache file
|
|
local function token_file_path()
|
|
local xdg = os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME") .. "/.config")
|
|
return xdg .. "/jasper/token.json"
|
|
end
|
|
|
|
--- Read the cached auth token from disk.
|
|
--- @return string|nil
|
|
function M.get_token()
|
|
local path = token_file_path()
|
|
if vim.fn.filereadable(path) ~= 1 then
|
|
return nil
|
|
end
|
|
|
|
local lines = vim.fn.readfile(path)
|
|
if not lines or #lines == 0 then
|
|
return nil
|
|
end
|
|
|
|
local ok, data = pcall(vim.fn.json_decode, table.concat(lines, "\n"))
|
|
if ok and type(data) == "table" and data.auth_token then
|
|
return data.auth_token
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Delete the cached token (e.g. on expiry).
|
|
function M.delete_token()
|
|
local path = token_file_path()
|
|
if vim.fn.filereadable(path) == 1 then
|
|
vim.fn.delete(path)
|
|
end
|
|
end
|
|
|
|
--- Perform a login request and cache the resulting token.
|
|
--- @param username string
|
|
--- @param password string
|
|
--- @return string|nil token
|
|
--- @return string|nil error message
|
|
function M.login(username, password)
|
|
-- Write credentials to a temp curl config file so they never appear in
|
|
-- the process argument list (ps aux / /proc/<pid>/cmdline).
|
|
local tmpfile = os.tmpname()
|
|
local cf = io.open(tmpfile, "w")
|
|
if not cf then
|
|
return nil, "cannot create temp file for curl config"
|
|
end
|
|
cf:write('--data-urlencode "username=' .. username .. '"\n')
|
|
cf:write('--data-urlencode "password=' .. password .. '"\n')
|
|
cf:close()
|
|
|
|
local result = vim.system({
|
|
"curl",
|
|
"-s",
|
|
"-X",
|
|
"POST",
|
|
"--config",
|
|
tmpfile,
|
|
JASPER_URL .. "/api/v1/token/login/",
|
|
}, { text = true }):wait()
|
|
|
|
os.remove(tmpfile)
|
|
|
|
if result.code ~= 0 then
|
|
return nil, "curl error (exit " .. result.code .. ")"
|
|
end
|
|
|
|
local ok, data = pcall(vim.fn.json_decode, result.stdout)
|
|
if not ok or type(data) ~= "table" or not data.auth_token then
|
|
return nil, "login failed (bad credentials or unexpected response)"
|
|
end
|
|
|
|
-- Save to cache (same format as Python script)
|
|
local path = token_file_path()
|
|
local dir = vim.fn.fnamemodify(path, ":h")
|
|
vim.fn.mkdir(dir, "p")
|
|
vim.fn.writefile({ vim.fn.json_encode(data) }, path)
|
|
|
|
return data.auth_token, nil
|
|
end
|
|
|
|
--- Try to retrieve a password from the system Secret Service via secret-tool.
|
|
--- @param username string
|
|
--- @return string|nil password
|
|
local function secret_tool_lookup(username)
|
|
local result = vim.system({ "secret-tool", "lookup", "service", "jasper", "user", username }, { text = true })
|
|
:wait()
|
|
if result.code == 0 and result.stdout and result.stdout ~= "" then
|
|
-- strip trailing newline
|
|
return (result.stdout:gsub("%s+$", ""))
|
|
end
|
|
return nil
|
|
end
|
|
|
|
--- Attempt login with the given username: try Secret Service first, then prompt.
|
|
--- @param username string
|
|
--- @param callback fun(token: string|nil)
|
|
local function login_with_username(username, callback)
|
|
local password = secret_tool_lookup(username)
|
|
if password then
|
|
local token, err = M.login(username, password)
|
|
if token and not err then
|
|
vim.notify("[Jasper] Login successful (Secret Service)", vim.log.levels.INFO)
|
|
callback(token)
|
|
return
|
|
end
|
|
-- Secret Service password was wrong or login failed — fall through to prompt
|
|
vim.notify("[Jasper] Secret Service password failed, prompting", vim.log.levels.WARN)
|
|
end
|
|
-- Prompt for password
|
|
local pwd = vim.fn.inputsecret("Jasper password: ")
|
|
if not pwd or pwd == "" then
|
|
vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN)
|
|
callback(nil)
|
|
return
|
|
end
|
|
local token, err = M.login(username, pwd)
|
|
if err or not token then
|
|
vim.notify("[Jasper] " .. (err or "Login failed"), vim.log.levels.ERROR)
|
|
callback(nil)
|
|
return
|
|
end
|
|
vim.notify("[Jasper] Login successful", vim.log.levels.INFO)
|
|
callback(token)
|
|
end
|
|
|
|
--- Interactively ask the user for credentials using vim.ui.input,
|
|
--- then call login(). Calls `callback(token)` on success, `callback(nil)` on failure.
|
|
--- Checks JASPER_USERNAME env var and Secret Service before prompting.
|
|
--- @param callback fun(token: string|nil)
|
|
function M.prompt_login(callback)
|
|
local env_username = os.getenv("JASPER_USERNAME")
|
|
if env_username and env_username ~= "" then
|
|
login_with_username(env_username, callback)
|
|
else
|
|
vim.ui.input({ prompt = "Jasper username: " }, function(username)
|
|
if not username or username == "" then
|
|
vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN)
|
|
callback(nil)
|
|
return
|
|
end
|
|
login_with_username(username, callback)
|
|
end)
|
|
end
|
|
end
|
|
|
|
return M
|