From 9178f4e5eaa0c4c5256885dfbe99654b0864f3ed Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Tue, 22 Sep 2026 10:09:28 +0530 Subject: [PATCH] fix: create users via the lnbits 1.x account api The bot creates telegram users through the usermanager extension, whose last release caps at max_lnbits_version 1.0.0. The backend now runs 1.6.0, where the extension cannot be installed at all - its imports of get_key_type and WalletTypeInfo no longer exist - so every /start from a new user failed with "Create wallet error: Not Found", a 404. Existing users were unaffected, since their wallets were already on the instance. Usermanager's functionality moved into core, behind account-level auth: a wallet api key does not grant it, so account management now needs a bearer token. Rather than put the superuser login in the config, take an ACL token scoped to /users/api/v1, which can manage accounts but cannot spend from any wallet or reach admin settings. The token is kept in its own header, so wallet calls cannot present it even by accident, and paying still goes through the wallet's own key. Two behaviours differ from usermanager and are handled here rather than at the call sites, which are unchanged: - creating a user also creates its wallet, but names it after lnbits_default_wallet_name and omits the wallets from the response, so the wallet is fetched and renamed to keep the existing " ()" convention. - the bot passes a telegram handle as the email, which 1.x rejects as malformed, so it is no longer sent. admin_id has no counterpart at all and is now ignored. --- config.yaml.example | 6 ++- internal/config.go | 4 ++ internal/lnbits/lnbits.go | 85 +++++++++++++++++++++++++++++++-------- internal/lnbits/types.go | 12 ++++-- internal/telegram/bot.go | 6 ++- 5 files changed, 90 insertions(+), 23 deletions(-) diff --git a/config.yaml.example b/config.yaml.example index ab8c4080..e2a4e965 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -18,7 +18,11 @@ telegram: lnbits: url: "http://127.0.0.1:5000" admin_key: "1234" - admin_id: "1234" + admin_id: "1234" # unused on lnbits 1.x, kept for older deployments + # ACL token scoped to /users/api/v1, required on lnbits 1.x to create users. + # Mint one under your lnbits account's access-control settings; a wallet api + # key does not grant account management there. + admin_token: "1234" webhook_server: "http://0.0.0.0:5588" # Local webhook server address webhook_public_url: "https://yourdomain.com/webhook" # Public URL for webhooks (for reverse proxy) lnbits_public_url: "link.mylnurl.com" diff --git a/internal/config.go b/internal/config.go index 36be4668..6d34209b 100644 --- a/internal/config.go +++ b/internal/config.go @@ -77,6 +77,10 @@ type DatabaseConfiguration struct { type LnbitsConfiguration struct { AdminId string `yaml:"admin_id"` AdminKey string `yaml:"admin_key"` + // AdminToken is an lnbits ACL token, scoped to /users/api/v1. Account + // management moved behind account-level auth in lnbits 1.x, where a wallet + // api key no longer grants it, so creating users needs a bearer token. + AdminToken string `yaml:"admin_token"` Url string `yaml:"url"` LnbitsPublicUrl string `yaml:"lnbits_public_url"` WebhookServer string `yaml:"webhook_server"` diff --git a/internal/lnbits/lnbits.go b/internal/lnbits/lnbits.go index 63763950..deb397f7 100644 --- a/internal/lnbits/lnbits.go +++ b/internal/lnbits/lnbits.go @@ -21,8 +21,9 @@ func parseLNbitsError(resp *req.Resp) Error { return reqErr } -// NewClient returns a new lnbits api client. Pass your API key and url here. -func NewClient(key, url string) *Client { +// NewClient returns a new lnbits api client. Pass your API key, the ACL token +// used for account management, and the url here. +func NewClient(key, adminToken, url string) *Client { return &Client{ url: url, // info: this header holds the ADMIN key for the entire API @@ -34,12 +35,17 @@ func NewClient(key, url string) *Client { "Accept": "application/json", "X-Api-Key": key, }, + adminHeader: req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "Authorization": "Bearer " + adminToken, + }, } } // GetUser returns user information func (c *Client) GetUser(userId string) (user User, err error) { - resp, err := req.Post(c.url+"/usermanager/api/v1/users/"+userId, c.header, nil) + resp, err := req.Get(c.url+"/users/api/v1/user/"+userId, c.adminHeader, nil) if err != nil { return } @@ -53,14 +59,21 @@ func (c *Client) GetUser(userId string) (user User, err error) { return } -// CreateUserWithInitialWallet creates new user with initial wallet +// CreateUserWithInitialWallet creates new user with initial wallet. +// +// lnbits 1.x creates the account's first wallet itself, named after +// lnbits_default_wallet_name, and its create-user response omits the wallets. +// So the wallet is fetched separately and renamed to walletName, keeping the +// " ()" convention the existing wallets use. +// +// adminId is unused: lnbits 1.x has no admin_id concept. It is kept so callers +// do not have to change. func (c *Client) CreateUserWithInitialWallet(userName, walletName, adminId string, email string) (wal User, err error) { - resp, err := req.Post(c.url+"/usermanager/api/v1/users", c.header, req.BodyJSON(struct { - WalletName string `json:"wallet_name"` - AdminId string `json:"admin_id"` - UserName string `json:"user_name"` - Email string `json:"email"` - }{walletName, adminId, userName, email})) + // email is deliberately not sent: the bot passes a telegram handle here, + // which lnbits 1.x rejects via is_valid_email_address. + resp, err := req.Post(c.url+"/users/api/v1/user", c.adminHeader, req.BodyJSON(struct { + UserName string `json:"username"` + }{userName})) if err != nil { return } @@ -69,17 +82,55 @@ func (c *Client) CreateUserWithInitialWallet(userName, walletName, adminId strin err = parseLNbitsError(resp) return } - err = resp.ToJSON(&wal) + if err = resp.ToJSON(&wal); err != nil { + return + } + + wallets, err := c.Wallets(wal) + if err != nil { + return + } + if len(wallets) == 0 { + err = fmt.Errorf("lnbits created user %s without a wallet", wal.ID) + return + } + if err = c.renameWallet(wallets[0], walletName); err != nil { + return + } + + // the create-user response echoes back only what it was given, so the name + // the bot stores is the username it asked for. + wal.Name = userName return } +// renameWallet sets a wallet's name. It authenticates with the wallet's own +// admin key rather than the ACL token, so it stays outside that token's scope. +func (c *Client) renameWallet(w Wallet, name string) error { + resp, err := req.Patch(c.url+"/api/v1/wallet", req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": w.Adminkey, + }, req.BodyJSON(struct { + Name string `json:"name"` + }{name})) + if err != nil { + return err + } + if resp.Response().StatusCode >= 300 { + return parseLNbitsError(resp) + } + return nil +} + // CreateWallet creates a new wallet. +// +// adminId is unused, see CreateUserWithInitialWallet. func (c *Client) CreateWallet(userId, walletName, adminId string) (wal Wallet, err error) { - resp, err := req.Post(c.url+"/usermanager/api/v1/wallets", c.header, req.BodyJSON(struct { - UserId string `json:"user_id"` - WalletName string `json:"wallet_name"` - AdminId string `json:"admin_id"` - }{userId, walletName, adminId})) + resp, err := req.Post(c.url+"/users/api/v1/user/"+userId+"/wallet", c.adminHeader, + req.BodyJSON(struct { + Name string `json:"name"` + }{walletName})) if err != nil { return } @@ -188,7 +239,7 @@ func (c Client) Payment(w Wallet, payment_hash string) (payment LNbitsPayment, e // Wallets returns all wallets belonging to an user func (c Client) Wallets(w User) (wtx []Wallet, err error) { - resp, err := req.Get(c.url+"/usermanager/api/v1/wallets/"+w.ID, c.header, nil) + resp, err := req.Get(c.url+"/users/api/v1/user/"+w.ID+"/wallet", c.adminHeader, nil) if err != nil { return } diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 6cdb15e3..7599c683 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -15,10 +15,14 @@ import ( ) type Client struct { - header req.Header - url string - AdminKey string - InvoiceKey string + header req.Header + // adminHeader carries the ACL bearer token for /users/api/v1. It is kept + // separate from header so wallet calls cannot present it, and so the token + // cannot be used to spend: it is scoped to account management only. + adminHeader req.Header + url string + AdminKey string + InvoiceKey string } type User struct { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 447ca68a..583bf8ed 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -51,7 +51,11 @@ func NewBot() TipBot { bot := TipBot{ DB: dbs, - Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), + Client: lnbits.NewClient( + internal.Configuration.Lnbits.AdminKey, + internal.Configuration.Lnbits.AdminToken, + internal.Configuration.Lnbits.Url, + ), Bunt: createBunt(internal.Configuration.Database.BuntDbPath), ShopBunt: createBunt(internal.Configuration.Database.ShopBuntDbPath), Telegram: newTelegramBot(),