alby-hub/http/alby_http_service.go
Roland 595361df27
feat: read only JWT for http api use (#1717)
* feat: read only JWT for http api use

* fix: pass permission to start and unlock endpoints

* chore: add http service JWT tests

* chore: update mocks

* fix: close db in tests
2025-09-19 13:07:10 +07:00

188 lines
6.3 KiB
Go

package http
import (
"errors"
"fmt"
"net/http"
"github.com/getAlby/hub/alby"
"github.com/getAlby/hub/config"
"github.com/getAlby/hub/logger"
"github.com/getAlby/hub/service"
"github.com/labstack/echo/v4"
)
type AlbyHttpService struct {
albySvc alby.AlbyService
albyOAuthSvc alby.AlbyOAuthService
appConfig *config.AppConfig
svc service.Service
}
func NewAlbyHttpService(svc service.Service, albySvc alby.AlbyService, albyOAuthSvc alby.AlbyOAuthService, appConfig *config.AppConfig) *AlbyHttpService {
return &AlbyHttpService{
albySvc: albySvc,
albyOAuthSvc: albyOAuthSvc,
appConfig: appConfig,
svc: svc,
}
}
func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(readOnlyApiGroup *echo.Group, fullAccessApiGroup *echo.Group, e *echo.Echo) {
e.GET("/api/alby/callback", albyHttpSvc.albyCallbackHandler)
e.GET("/api/alby/info", albyHttpSvc.albyInfoHandler)
e.GET("/api/alby/rates", albyHttpSvc.albyBitcoinRateHandler)
readOnlyApiGroup.GET("/alby/me", albyHttpSvc.albyMeHandler)
readOnlyApiGroup.GET("/alby/balance", albyHttpSvc.albyBalanceHandler)
fullAccessApiGroup.POST("/alby/pay", albyHttpSvc.albyPayHandler)
fullAccessApiGroup.POST("/alby/link-account", albyHttpSvc.albyLinkAccountHandler)
fullAccessApiGroup.POST("/alby/auto-channel", albyHttpSvc.autoChannelHandler)
fullAccessApiGroup.POST("/alby/unlink-account", albyHttpSvc.unlinkHandler)
}
func (albyHttpSvc *AlbyHttpService) autoChannelHandler(c echo.Context) error {
ctx := c.Request().Context()
var autoChannelRequest alby.AutoChannelRequest
if err := c.Bind(&autoChannelRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
autoChannelResponseResponse, err := albyHttpSvc.albyOAuthSvc.RequestAutoChannel(ctx, albyHttpSvc.svc.GetLNClient(), autoChannelRequest.IsPublic)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to request auto channel: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, autoChannelResponseResponse)
}
func (albyHttpSvc *AlbyHttpService) unlinkHandler(c echo.Context) error {
ctx := c.Request().Context()
err := albyHttpSvc.albyOAuthSvc.UnlinkAccount(ctx)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to unlink: %s", err.Error()),
})
}
return c.NoContent(http.StatusNoContent)
}
func (albyHttpSvc *AlbyHttpService) albyInfoHandler(c echo.Context) error {
info, err := albyHttpSvc.albySvc.GetInfo(c.Request().Context())
if err != nil {
logger.Logger.WithError(err).Error("Failed to request alby info endpoint")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to request alby info endpoint: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, info)
}
func (albyHttpSvc *AlbyHttpService) albyBitcoinRateHandler(c echo.Context) error {
rate, err := albyHttpSvc.albySvc.GetBitcoinRate(c.Request().Context())
if err != nil {
logger.Logger.WithError(err).Error("Failed to get Bitcoin rate")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to get Bitcoin rate: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, rate)
}
func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error {
code := c.QueryParam("code")
err := albyHttpSvc.albyOAuthSvc.CallbackHandler(c.Request().Context(), code, albyHttpSvc.svc.GetLNClient())
if err != nil {
logger.Logger.WithError(err).Error("Failed to handle Alby OAuth callback")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to handle Alby OAuth callback: %s", err.Error()),
})
}
if albyHttpSvc.appConfig.IsDefaultClientId() {
// do not redirect if using default OAuth client
// redirect will be handled by the frontend instead
return c.NoContent(http.StatusNoContent)
}
redirectUrl := albyHttpSvc.appConfig.GetBaseFrontendUrl()
if redirectUrl == "" {
// OAuth using a custom client requires a base URL set for the callback
return errors.New("no BASE_URL set")
}
return c.Redirect(http.StatusFound, redirectUrl)
}
func (albyHttpSvc *AlbyHttpService) albyMeHandler(c echo.Context) error {
me, err := albyHttpSvc.albyOAuthSvc.GetMe(c.Request().Context())
if err != nil {
logger.Logger.WithError(err).Error("Failed to request alby me endpoint")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to request alby me endpoint: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, me)
}
func (albyHttpSvc *AlbyHttpService) albyBalanceHandler(c echo.Context) error {
balance, err := albyHttpSvc.albyOAuthSvc.GetBalance(c.Request().Context())
if err != nil {
logger.Logger.WithError(err).Error("Failed to request alby balance endpoint")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to request alby balance endpoint: %s", err.Error()),
})
}
return c.JSON(http.StatusOK, &alby.AlbyBalanceResponse{
Sats: balance.Balance,
})
}
func (albyHttpSvc *AlbyHttpService) albyPayHandler(c echo.Context) error {
var payRequest alby.AlbyPayRequest
if err := c.Bind(&payRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
err := albyHttpSvc.albyOAuthSvc.SendPayment(c.Request().Context(), payRequest.Invoice)
if err != nil {
logger.Logger.WithError(err).Error("Failed to request alby pay endpoint")
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to request alby pay endpoint: %s", err.Error()),
})
}
return c.NoContent(http.StatusNoContent)
}
func (albyHttpSvc *AlbyHttpService) albyLinkAccountHandler(c echo.Context) error {
var linkAccountRequest alby.AlbyLinkAccountRequest
if err := c.Bind(&linkAccountRequest); err != nil {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: fmt.Sprintf("Bad request: %s", err.Error()),
})
}
err := albyHttpSvc.albyOAuthSvc.LinkAccount(c.Request().Context(), albyHttpSvc.svc.GetLNClient(), linkAccountRequest.Budget, linkAccountRequest.Renewal)
if err != nil {
logger.Logger.WithError(err).Error("Failed to connect alby account")
return err
}
return c.NoContent(http.StatusNoContent)
}