http_service.go raw
1 package http
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "net/http"
11 "slices"
12 "strconv"
13 "strings"
14 "sync"
15 "time"
16
17 "github.com/getAlby/go-nostr"
18 "github.com/golang-jwt/jwt/v5"
19 "github.com/labstack/echo/v4"
20 "github.com/labstack/echo/v4/middleware"
21 "github.com/sirupsen/logrus"
22 "gorm.io/gorm"
23
24 "github.com/getAlby/hub/apps"
25 "github.com/getAlby/hub/config"
26 "github.com/getAlby/hub/events"
27 "github.com/getAlby/hub/logger"
28 "github.com/getAlby/hub/service"
29
30 "github.com/getAlby/hub/api"
31 "github.com/getAlby/hub/frontend"
32 )
33
34 type authTokenResponse struct {
35 Token string `json:"token"`
36 }
37
38 type jwtCustomClaims struct {
39 // we can add extra claims here
40 // Name string `json:"name"`
41 // Admin bool `json:"admin"`
42 Permission string `json:"permission,omitempty"` // "full" or "readonly"
43 jwt.RegisteredClaims
44 }
45
46 type HttpService struct {
47 api api.API
48 albyHttpSvc *AlbyHttpService
49 cfg config.Config
50 eventPublisher events.EventPublisher
51 db *gorm.DB
52 appsSvc apps.AppsService
53
54 nip07Challenges map[string]time.Time
55 nip07ChallengesMutex sync.Mutex
56 }
57
58 func NewHttpService(svc service.Service, eventPublisher events.EventPublisher) *HttpService {
59 return &HttpService{
60 api: api.NewAPI(svc, svc.GetDB(), svc.GetConfig(), svc.GetKeys(), svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), eventPublisher),
61 albyHttpSvc: NewAlbyHttpService(svc, svc.GetAlbySvc(), svc.GetAlbyOAuthSvc(), svc.GetConfig().GetEnv()),
62 cfg: svc.GetConfig(),
63 eventPublisher: eventPublisher,
64 db: svc.GetDB(),
65 appsSvc: apps.NewAppsService(svc.GetDB(), eventPublisher, svc.GetKeys(), svc.GetConfig()),
66 nip07Challenges: map[string]time.Time{},
67 }
68 }
69
70 func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
71 e.HideBanner = true
72
73 e.Use(middleware.SecureWithConfig(middleware.SecureConfig{
74 ContentTypeNosniff: "nosniff",
75 XFrameOptions: "DENY",
76 // when making changes here, also update the CSP in frontend/vite.config.ts
77 ContentSecurityPolicy: "default-src 'self'; img-src 'self' https://uploads.getalby-assets.com https://cdn.getalby-assets.com https://getalby.com; connect-src 'self' https://api.getalby.com https://getalby.com https://zapplanner.albylabs.com wss://relay.mleku.dev wss://relay.getalby.com wss://relay2.getalby.com; frame-src https://www.youtube-nocookie.com",
78 ReferrerPolicy: "no-referrer",
79 }))
80 e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
81 LogURI: true,
82 LogStatus: true,
83 LogRemoteIP: true,
84 LogUserAgent: true,
85 LogHost: true,
86 LogRequestID: true,
87 LogValuesFunc: func(c echo.Context, values middleware.RequestLoggerValues) error {
88 logger.Logger.WithFields(logrus.Fields{
89 "uri": values.URI,
90 "status": values.Status,
91 "remote_ip": values.RemoteIP,
92 "user_agent": values.UserAgent,
93 "host": values.Host,
94 "request_id": values.RequestID,
95 }).Info("handled API request")
96 return nil
97 },
98 }))
99
100 e.Use(middleware.Recover())
101 e.Use(middleware.RequestID())
102
103 e.GET("/api/info", httpSvc.infoHandler)
104 e.POST("/api/setup", httpSvc.setupHandler)
105 e.POST("/api/restore", httpSvc.restoreBackupHandler)
106
107 // A single global rate limiter (one bucket for all callers, not per-IP)
108 // shared by every endpoint that verifies the unlock password, to bound how
109 // fast the password can be guessed.
110 unlockRateLimiter := middleware.RateLimiterWithConfig(middleware.RateLimiterConfig{
111 Store: middleware.NewRateLimiterMemoryStoreWithConfig(
112 // burst of 2 so unlocking and then immediately acting is not blocked
113 middleware.RateLimiterMemoryStoreConfig{Rate: 1, Burst: 2},
114 ),
115 IdentifierExtractor: func(c echo.Context) (string, error) {
116 return "", nil
117 },
118 })
119 e.POST("/api/start", httpSvc.startHandler, unlockRateLimiter)
120 e.POST("/api/unlock", httpSvc.unlockHandler, unlockRateLimiter)
121 e.POST("/api/backup", httpSvc.createBackupHandler, unlockRateLimiter)
122 e.GET("/logout", httpSvc.logoutHandler)
123
124 e.GET("/api/nip07/challenge", httpSvc.nip07ChallengeHandler)
125 e.POST("/api/nip07/auth", httpSvc.nip07AuthHandler)
126
127 frontend.RegisterHandlers(e)
128
129 // restricted routes: accept either a password-issued Bearer JWT or a
130 // NIP-07 session cookie
131 readOnlyApiGroup := e.Group("/api")
132 readOnlyApiGroup.Use(httpSvc.authMiddleware)
133
134 readOnlyApiGroup.GET("/apps", httpSvc.appsListHandler)
135 readOnlyApiGroup.GET("/apps/:pubkey", httpSvc.appsShowByPubkeyHandler)
136 readOnlyApiGroup.GET("/v2/apps/:id", httpSvc.appsShowHandler)
137 readOnlyApiGroup.GET("/channels", httpSvc.channelsListHandler)
138 readOnlyApiGroup.GET("/channels/suggestions", httpSvc.channelPeerSuggestionsHandler)
139 readOnlyApiGroup.GET("/channel-offer", httpSvc.channelOfferHandler)
140 readOnlyApiGroup.GET("/node/connection-info", httpSvc.nodeConnectionInfoHandler)
141 readOnlyApiGroup.GET("/node/status", httpSvc.nodeStatusHandler)
142 readOnlyApiGroup.GET("/node/network-graph", httpSvc.nodeNetworkGraphHandler)
143 readOnlyApiGroup.GET("/node/transactions", httpSvc.listOnchainTransactionsHandler)
144 readOnlyApiGroup.GET("/peers", httpSvc.listPeers)
145 readOnlyApiGroup.GET("/wallet/address", httpSvc.onchainAddressHandler)
146 readOnlyApiGroup.GET("/wallet/capabilities", httpSvc.capabilitiesHandler)
147 readOnlyApiGroup.GET("/transactions", httpSvc.listTransactionsHandler)
148 readOnlyApiGroup.GET("/transactions/:paymentHash", httpSvc.lookupTransactionHandler)
149 readOnlyApiGroup.GET("/balances", httpSvc.balancesHandler)
150 readOnlyApiGroup.GET("/mempool", httpSvc.mempoolApiHandler)
151 readOnlyApiGroup.GET("/health", httpSvc.healthHandler)
152 readOnlyApiGroup.GET("/commands", httpSvc.getCustomNodeCommandsHandler)
153 readOnlyApiGroup.GET("/swaps", httpSvc.listSwapsHandler)
154 readOnlyApiGroup.GET("/swaps/:swapId", httpSvc.lookupSwapHandler)
155 readOnlyApiGroup.GET("/swaps/out/info", httpSvc.getSwapOutInfoHandler)
156 readOnlyApiGroup.GET("/swaps/in/info", httpSvc.getSwapInInfoHandler)
157 readOnlyApiGroup.GET("/autoswap", httpSvc.getAutoSwapConfigHandler)
158 readOnlyApiGroup.GET("/forwards", httpSvc.forwardsHandler)
159
160 // Full access API group - requires a token with full permissions
161 fullAccessApiGroup := e.Group("/api")
162 fullAccessApiGroup.Use(httpSvc.authMiddleware)
163 fullAccessApiGroup.Use(httpSvc.requireFullAccess)
164
165 fullAccessApiGroup.POST("/event", httpSvc.eventHandler)
166 fullAccessApiGroup.PATCH("/unlock-password", httpSvc.changeUnlockPasswordHandler, unlockRateLimiter)
167 fullAccessApiGroup.PATCH("/auto-unlock", httpSvc.autoUnlockHandler, unlockRateLimiter)
168 fullAccessApiGroup.PATCH("/settings", httpSvc.updateSettingsHandler)
169 fullAccessApiGroup.PATCH("/apps/:pubkey", httpSvc.appsUpdateHandler)
170 fullAccessApiGroup.PATCH("/transactions/:id/labels", httpSvc.setTransactionUserLabelsHandler)
171 fullAccessApiGroup.DELETE("/apps/:pubkey", httpSvc.appsDeleteHandler)
172 fullAccessApiGroup.POST("/transfers", httpSvc.transfersHandler)
173 fullAccessApiGroup.POST("/apps", httpSvc.appsCreateHandler, unlockRateLimiter)
174 fullAccessApiGroup.POST("/lightning-addresses", httpSvc.lightningAddressesCreateHandler)
175 fullAccessApiGroup.DELETE("/lightning-addresses/:appId", httpSvc.lightningAddressesDeleteHandler)
176 fullAccessApiGroup.POST("/mnemonic", httpSvc.mnemonicHandler, unlockRateLimiter)
177 fullAccessApiGroup.PATCH("/backup-reminder", httpSvc.backupReminderHandler)
178 fullAccessApiGroup.POST("/channels", httpSvc.openChannelHandler)
179 fullAccessApiGroup.POST("/channels/rebalance", httpSvc.rebalanceChannelHandler)
180 fullAccessApiGroup.POST("/lsp-orders", httpSvc.newInstantChannelInvoiceHandler)
181 fullAccessApiGroup.POST("/node/migrate-storage", httpSvc.migrateNodeStorageHandler)
182 fullAccessApiGroup.POST("/peers", httpSvc.connectPeerHandler)
183 fullAccessApiGroup.DELETE("/peers/:peerId", httpSvc.disconnectPeerHandler)
184 fullAccessApiGroup.DELETE("/peers/:peerId/channels/:channelId", httpSvc.closeChannelHandler)
185 fullAccessApiGroup.PATCH("/peers/:peerId/channels/:channelId", httpSvc.updateChannelHandler)
186 fullAccessApiGroup.POST("/wallet/new-address", httpSvc.newOnchainAddressHandler)
187 fullAccessApiGroup.POST("/wallet/redeem-onchain-funds", httpSvc.redeemOnchainFundsHandler)
188 fullAccessApiGroup.POST("/wallet/sign-message", httpSvc.signMessageHandler)
189 fullAccessApiGroup.POST("/wallet/sync", httpSvc.walletSyncHandler)
190 fullAccessApiGroup.POST("/payments/:invoice", httpSvc.sendPaymentHandler)
191 fullAccessApiGroup.POST("/invoices", httpSvc.makeInvoiceHandler)
192 fullAccessApiGroup.POST("/offers", httpSvc.makeOfferHandler)
193 fullAccessApiGroup.POST("/reset-router", httpSvc.resetRouterHandler)
194 fullAccessApiGroup.POST("/stop", httpSvc.stopHandler)
195 fullAccessApiGroup.POST("/command", httpSvc.execCustomNodeCommandHandler)
196 fullAccessApiGroup.POST("/swaps/out", httpSvc.initiateSwapOutHandler)
197 fullAccessApiGroup.POST("/swaps/in", httpSvc.initiateSwapInHandler)
198 fullAccessApiGroup.POST("/swaps/refund", httpSvc.refundSwapHandler)
199 fullAccessApiGroup.GET("/swaps/mnemonic", httpSvc.swapMnemonicHandler)
200 fullAccessApiGroup.GET("/log/:type", httpSvc.getLogOutputHandler)
201 fullAccessApiGroup.POST("/autoswap", httpSvc.enableAutoSwapOutHandler, unlockRateLimiter)
202 fullAccessApiGroup.DELETE("/autoswap", httpSvc.disableAutoSwapOutHandler)
203 fullAccessApiGroup.POST("/node/alias", httpSvc.setNodeAliasHandler)
204
205 httpSvc.albyHttpSvc.RegisterSharedRoutes(readOnlyApiGroup, fullAccessApiGroup, e)
206 }
207
208 func (httpSvc *HttpService) infoHandler(c echo.Context) error {
209 responseBody, err := httpSvc.api.GetInfo(c.Request().Context())
210 if err != nil {
211 return c.JSON(http.StatusInternalServerError, ErrorResponse{
212 Message: err.Error(),
213 })
214 }
215
216 responseBody.Unlocked = httpSvc.isAuthenticated(c)
217
218 return c.JSON(http.StatusOK, responseBody)
219 }
220
221 const (
222 nip07AuthKind = 27235
223 nip07SessionCookie = "albyhub_session"
224 nip07ChallengeTTL = 5 * time.Minute
225 nip07EventMaxAge = 5 * time.Minute
226 nip07SessionExpiry = 30 * 24 * time.Hour
227 )
228
229 type nip07ChallengeResponse struct {
230 Challenge string `json:"challenge"`
231 Pubkey string `json:"pubkey"`
232 }
233
234 type nip07AuthRequest struct {
235 Challenge string `json:"challenge"`
236 Event nostr.Event `json:"event"`
237 }
238
239 // isAuthenticated reports whether the request carries a valid session, either
240 // a password-issued Bearer JWT or a NIP-07 session cookie.
241 func (httpSvc *HttpService) isAuthenticated(c echo.Context) bool {
242 authHeader := c.Request().Header.Get("Authorization")
243 if strings.HasPrefix(authHeader, "Bearer ") {
244 tokenString := strings.TrimPrefix(authHeader, "Bearer ")
245 if secret, err := httpSvc.cfg.GetJWTSecret(); err == nil && secret != "" {
246 if httpSvc.parseJWT(tokenString, secret) != nil {
247 return true
248 }
249 }
250 }
251 if cookie, err := c.Cookie(nip07SessionCookie); err == nil && cookie.Value != "" {
252 if secret, err := httpSvc.cfg.GetSessionSecret(); err == nil && secret != "" {
253 if httpSvc.parseJWT(cookie.Value, secret) != nil {
254 return true
255 }
256 }
257 }
258 return false
259 }
260
261 func (httpSvc *HttpService) authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
262 return func(c echo.Context) error {
263 authHeader := c.Request().Header.Get("Authorization")
264 if strings.HasPrefix(authHeader, "Bearer ") {
265 tokenString := strings.TrimPrefix(authHeader, "Bearer ")
266 if secret, err := httpSvc.cfg.GetJWTSecret(); err == nil && secret != "" {
267 if token := httpSvc.parseJWT(tokenString, secret); token != nil {
268 c.Set("user", token)
269 return next(c)
270 }
271 }
272 }
273 if cookie, err := c.Cookie(nip07SessionCookie); err == nil && cookie.Value != "" {
274 if secret, err := httpSvc.cfg.GetSessionSecret(); err == nil && secret != "" {
275 if token := httpSvc.parseJWT(cookie.Value, secret); token != nil {
276 c.Set("user", token)
277 return next(c)
278 }
279 }
280 }
281 return c.JSON(http.StatusUnauthorized, ErrorResponse{
282 Message: "unauthorized",
283 })
284 }
285 }
286
287 func (httpSvc *HttpService) parseJWT(tokenString, secret string) *jwt.Token {
288 token, err := jwt.ParseWithClaims(tokenString, &jwtCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
289 return []byte(secret), nil
290 })
291 if err != nil || !token.Valid {
292 return nil
293 }
294 return token
295 }
296
297 func (httpSvc *HttpService) nip07ChallengeHandler(c echo.Context) error {
298 pubkey := httpSvc.cfg.GetNip07OwnerPubkey()
299 if pubkey == "" {
300 return c.JSON(http.StatusBadRequest, ErrorResponse{
301 Message: "NIP-07 auth is not configured",
302 })
303 }
304
305 challengeBytes := make([]byte, 32)
306 if _, err := rand.Read(challengeBytes); err != nil {
307 return c.JSON(http.StatusInternalServerError, ErrorResponse{
308 Message: "Failed to generate challenge",
309 })
310 }
311 challenge := hex.EncodeToString(challengeBytes)
312
313 httpSvc.nip07ChallengesMutex.Lock()
314 httpSvc.nip07Challenges[challenge] = time.Now().Add(nip07ChallengeTTL)
315 httpSvc.nip07ChallengesMutex.Unlock()
316
317 return c.JSON(http.StatusOK, nip07ChallengeResponse{
318 Challenge: challenge,
319 Pubkey: pubkey,
320 })
321 }
322
323 func (httpSvc *HttpService) nip07AuthHandler(c echo.Context) error {
324 ownerPubkey := httpSvc.cfg.GetNip07OwnerPubkey()
325 if ownerPubkey == "" {
326 return c.JSON(http.StatusBadRequest, ErrorResponse{
327 Message: "NIP-07 auth is not configured",
328 })
329 }
330
331 var req nip07AuthRequest
332 if err := c.Bind(&req); err != nil {
333 return c.JSON(http.StatusBadRequest, ErrorResponse{
334 Message: fmt.Sprintf("Bad request: %s", err.Error()),
335 })
336 }
337
338 httpSvc.nip07ChallengesMutex.Lock()
339 expiresAt, ok := httpSvc.nip07Challenges[req.Challenge]
340 if ok {
341 delete(httpSvc.nip07Challenges, req.Challenge)
342 }
343 httpSvc.nip07ChallengesMutex.Unlock()
344 if !ok || time.Now().After(expiresAt) {
345 return c.JSON(http.StatusUnauthorized, ErrorResponse{
346 Message: "invalid or expired challenge",
347 })
348 }
349
350 if req.Event.PubKey != ownerPubkey {
351 return c.JSON(http.StatusUnauthorized, ErrorResponse{
352 Message: "event not signed by the configured owner",
353 })
354 }
355 if req.Event.Kind != nip07AuthKind {
356 return c.JSON(http.StatusUnauthorized, ErrorResponse{
357 Message: "unexpected event kind",
358 })
359 }
360 valid, err := req.Event.CheckSignature()
361 if err != nil || !valid {
362 return c.JSON(http.StatusUnauthorized, ErrorResponse{
363 Message: "invalid event signature",
364 })
365 }
366 if req.Event.CreatedAt.Time().Before(time.Now().Add(-nip07EventMaxAge)) ||
367 req.Event.CreatedAt.Time().After(time.Now().Add(nip07EventMaxAge)) {
368 return c.JSON(http.StatusUnauthorized, ErrorResponse{
369 Message: "event timestamp outside allowed window",
370 })
371 }
372 challengeTagged := false
373 for _, tag := range req.Event.Tags {
374 if len(tag) == 2 && tag[0] == "challenge" && tag[1] == req.Challenge {
375 challengeTagged = true
376 break
377 }
378 }
379 if !challengeTagged {
380 return c.JSON(http.StatusUnauthorized, ErrorResponse{
381 Message: "event does not sign the challenge",
382 })
383 }
384
385 token, err := httpSvc.createSessionJWT(nil, "full")
386 if err != nil {
387 return c.JSON(http.StatusInternalServerError, ErrorResponse{
388 Message: fmt.Sprintf("Failed to create session: %s", err.Error()),
389 })
390 }
391
392 cookie := &http.Cookie{
393 Name: nip07SessionCookie,
394 Value: token,
395 Path: "/",
396 HttpOnly: true,
397 Secure: c.IsTLS() || c.Request().Header.Get("X-Forwarded-Proto") == "https",
398 SameSite: http.SameSiteLaxMode,
399 Expires: time.Now().Add(nip07SessionExpiry),
400 }
401 c.SetCookie(cookie)
402
403 return c.JSON(http.StatusOK, map[string]string{"pubkey": ownerPubkey})
404 }
405
406 func (httpSvc *HttpService) eventHandler(c echo.Context) error {
407 var sendEventRequest api.SendEventRequest
408 if err := c.Bind(&sendEventRequest); err != nil {
409 return c.JSON(http.StatusBadRequest, ErrorResponse{
410 Message: fmt.Sprintf("Bad request: %s", err.Error()),
411 })
412 }
413
414 httpSvc.api.SendEvent(sendEventRequest.Event, sendEventRequest.Properties)
415
416 return c.NoContent(http.StatusOK)
417 }
418
419 func (httpSvc *HttpService) mnemonicHandler(c echo.Context) error {
420 var mnemonicRequest api.MnemonicRequest
421 if err := c.Bind(&mnemonicRequest); err != nil {
422 return c.JSON(http.StatusBadRequest, ErrorResponse{
423 Message: fmt.Sprintf("Bad request: %s", err.Error()),
424 })
425 }
426
427 responseBody, err := httpSvc.api.GetMnemonic(mnemonicRequest.UnlockPassword)
428
429 if err != nil {
430 return c.JSON(http.StatusInternalServerError, ErrorResponse{
431 Message: err.Error(),
432 })
433 }
434
435 return c.JSON(http.StatusOK, responseBody)
436 }
437
438 func (httpSvc *HttpService) backupReminderHandler(c echo.Context) error {
439 var backupReminderRequest api.BackupReminderRequest
440 if err := c.Bind(&backupReminderRequest); err != nil {
441 return c.JSON(http.StatusBadRequest, ErrorResponse{
442 Message: fmt.Sprintf("Bad request: %s", err.Error()),
443 })
444 }
445
446 err := httpSvc.api.SetNextBackupReminder(&backupReminderRequest)
447 if err != nil {
448 return c.JSON(http.StatusInternalServerError, ErrorResponse{
449 Message: fmt.Sprintf("Failed to store backup reminder: %s", err.Error()),
450 })
451 }
452
453 return c.NoContent(http.StatusNoContent)
454 }
455
456 func (httpSvc *HttpService) startHandler(c echo.Context) error {
457 var startRequest api.StartRequest
458 if err := c.Bind(&startRequest); err != nil {
459 return c.JSON(http.StatusBadRequest, ErrorResponse{
460 Message: fmt.Sprintf("Bad request: %s", err.Error()),
461 })
462 }
463
464 if !httpSvc.cfg.CheckUnlockPassword(startRequest.UnlockPassword) {
465 return c.JSON(http.StatusUnauthorized, ErrorResponse{
466 Message: "Invalid password",
467 })
468 }
469
470 go httpSvc.api.Start(&startRequest)
471
472 // with NIP-07 auth configured the unlock password only starts the node;
473 // the UI session must be established via NIP-07 instead
474 if httpSvc.cfg.GetNip07OwnerPubkey() != "" {
475 return c.NoContent(http.StatusNoContent)
476 }
477
478 err := httpSvc.cfg.LoadJWTSecret(startRequest.UnlockPassword)
479
480 if err != nil {
481 return c.JSON(http.StatusInternalServerError, ErrorResponse{
482 Message: fmt.Sprintf("Failed to load JWT secret: %s", err.Error()),
483 })
484 }
485
486 token, err := httpSvc.createJWT(nil, "full")
487
488 if err != nil {
489 return c.JSON(http.StatusInternalServerError, ErrorResponse{
490 Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
491 })
492 }
493
494 return c.JSON(http.StatusOK, &authTokenResponse{
495 Token: token,
496 })
497 }
498
499 func (httpSvc *HttpService) unlockHandler(c echo.Context) error {
500 if httpSvc.cfg.GetNip07OwnerPubkey() != "" {
501 return c.JSON(http.StatusForbidden, ErrorResponse{
502 Message: "Password unlock is disabled. Sign in with Nostr.",
503 })
504 }
505
506 var unlockRequest api.UnlockRequest
507 if err := c.Bind(&unlockRequest); err != nil {
508 return c.JSON(http.StatusBadRequest, ErrorResponse{
509 Message: fmt.Sprintf("Bad request: %s", err.Error()),
510 })
511 }
512
513 if !httpSvc.cfg.CheckUnlockPassword(unlockRequest.UnlockPassword) {
514 return c.JSON(http.StatusUnauthorized, ErrorResponse{
515 Message: "Invalid password",
516 })
517 }
518
519 if unlockRequest.Permission == "" {
520 return c.JSON(http.StatusBadRequest, ErrorResponse{
521 Message: "Permission field is required",
522 })
523 }
524
525 if !slices.Contains([]string{"full", "readonly"}, unlockRequest.Permission) {
526 return c.JSON(http.StatusBadRequest, ErrorResponse{
527 Message: "Permission field is unknown",
528 })
529 }
530
531 _, err := httpSvc.api.GetNodeStatus(c.Request().Context())
532 if err != nil {
533 if errors.Is(err, api.ErrLNClientNotStarted) {
534 return c.JSON(http.StatusBadRequest, ErrorResponse{
535 Message: "Node is not running, start it before unlocking.",
536 })
537 }
538
539 return c.JSON(http.StatusInternalServerError, ErrorResponse{
540 Message: err.Error(),
541 })
542 }
543
544 token, err := httpSvc.createJWT(unlockRequest.TokenExpiryDays, unlockRequest.Permission)
545 if err != nil {
546 return c.JSON(http.StatusInternalServerError, ErrorResponse{
547 Message: fmt.Sprintf("Failed to save session: %s", err.Error()),
548 })
549 }
550
551 httpSvc.eventPublisher.Publish(&events.Event{
552 Event: "nwc_unlocked",
553 })
554
555 return c.JSON(http.StatusOK, &authTokenResponse{
556 Token: token,
557 })
558 }
559
560 func (httpSvc *HttpService) requireFullAccess(next echo.HandlerFunc) echo.HandlerFunc {
561 return func(c echo.Context) error {
562 token := c.Get("user").(*jwt.Token)
563 claims := token.Claims.(*jwtCustomClaims)
564
565 // Allow if no permission specified (backward compatibility) or if full access
566 if claims.Permission == "" || claims.Permission == "full" {
567 return next(c)
568 }
569
570 return c.JSON(http.StatusForbidden, ErrorResponse{
571 Message: "This operation requires full access permissions",
572 })
573 }
574 }
575
576 func (httpSvc *HttpService) changeUnlockPasswordHandler(c echo.Context) error {
577 var changeUnlockPasswordRequest api.ChangeUnlockPasswordRequest
578 if err := c.Bind(&changeUnlockPasswordRequest); err != nil {
579 return c.JSON(http.StatusBadRequest, ErrorResponse{
580 Message: fmt.Sprintf("Bad request: %s", err.Error()),
581 })
582 }
583
584 err := httpSvc.api.ChangeUnlockPassword(&changeUnlockPasswordRequest)
585 if err != nil {
586 return c.JSON(http.StatusInternalServerError, ErrorResponse{
587 Message: fmt.Sprintf("Failed to change unlock password: %s", err.Error()),
588 })
589 }
590
591 return c.NoContent(http.StatusNoContent)
592 }
593
594 func (httpSvc *HttpService) updateSettingsHandler(c echo.Context) error {
595 var updateSettingsRequest api.UpdateSettingsRequest
596 if err := c.Bind(&updateSettingsRequest); err != nil {
597 return c.JSON(http.StatusBadRequest, ErrorResponse{
598 Message: fmt.Sprintf("Bad request: %s", err.Error()),
599 })
600 }
601
602 err := httpSvc.api.UpdateSettings(&updateSettingsRequest)
603 if err != nil {
604 return c.JSON(http.StatusInternalServerError, ErrorResponse{
605 Message: fmt.Sprintf("Failed to update settings: %s", err.Error()),
606 })
607 }
608
609 return c.NoContent(http.StatusNoContent)
610 }
611
612 func (httpSvc *HttpService) autoUnlockHandler(c echo.Context) error {
613 var autoUnlockRequest api.AutoUnlockRequest
614 if err := c.Bind(&autoUnlockRequest); err != nil {
615 return c.JSON(http.StatusBadRequest, ErrorResponse{
616 Message: fmt.Sprintf("Bad request: %s", err.Error()),
617 })
618 }
619
620 err := httpSvc.api.SetAutoUnlockPassword(autoUnlockRequest.UnlockPassword)
621 if err != nil {
622 return c.JSON(http.StatusInternalServerError, ErrorResponse{
623 Message: fmt.Sprintf("Failed to set auto unlock password: %s", err.Error()),
624 })
625 }
626
627 return c.NoContent(http.StatusNoContent)
628 }
629
630 func (httpSvc *HttpService) createJWT(tokenExpiryDays *uint64, permission string) (string, error) {
631 secret, err := httpSvc.cfg.GetJWTSecret()
632 if err != nil {
633 return "", err
634 }
635 return signSessionJWT(secret, tokenExpiryDays, permission)
636 }
637
638 func (httpSvc *HttpService) createSessionJWT(tokenExpiryDays *uint64, permission string) (string, error) {
639 secret, err := httpSvc.cfg.GetSessionSecret()
640 if err != nil {
641 return "", err
642 }
643 return signSessionJWT(secret, tokenExpiryDays, permission)
644 }
645
646 func signSessionJWT(secret string, tokenExpiryDays *uint64, permission string) (string, error) {
647 if !slices.Contains([]string{"full", "readonly"}, permission) {
648 return "", errors.New("invalid token permission")
649 }
650
651 expiryDays := uint64(30)
652 if tokenExpiryDays != nil {
653 expiryDays = *tokenExpiryDays
654 }
655
656 // Set custom claims
657 claims := &jwtCustomClaims{
658 Permission: permission,
659 RegisteredClaims: jwt.RegisteredClaims{
660 ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * time.Duration(expiryDays))),
661 },
662 }
663
664 // Create token with claims
665 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
666
667 if token == nil {
668 return "", errors.New("failed to create token")
669 }
670
671 return token.SignedString([]byte(secret))
672 }
673
674 func (httpSvc *HttpService) channelsListHandler(c echo.Context) error {
675 ctx := c.Request().Context()
676
677 channels, err := httpSvc.api.ListChannels(ctx)
678
679 if err != nil {
680 return c.JSON(http.StatusInternalServerError, ErrorResponse{
681 Message: err.Error(),
682 })
683 }
684
685 return c.JSON(http.StatusOK, channels)
686 }
687
688 func (httpSvc *HttpService) channelPeerSuggestionsHandler(c echo.Context) error {
689 ctx := c.Request().Context()
690
691 suggestions, err := httpSvc.api.GetChannelPeerSuggestions(ctx)
692
693 if err != nil {
694 return c.JSON(http.StatusInternalServerError, ErrorResponse{
695 Message: err.Error(),
696 })
697 }
698
699 return c.JSON(http.StatusOK, suggestions)
700 }
701
702 func (httpSvc *HttpService) channelOfferHandler(c echo.Context) error {
703 ctx := c.Request().Context()
704
705 suggestions, err := httpSvc.api.GetLSPChannelOffer(ctx)
706
707 if err != nil {
708 return c.JSON(http.StatusInternalServerError, ErrorResponse{
709 Message: err.Error(),
710 })
711 }
712
713 return c.JSON(http.StatusOK, suggestions)
714 }
715
716 func (httpSvc *HttpService) resetRouterHandler(c echo.Context) error {
717 var resetRouterRequest api.ResetRouterRequest
718 if err := c.Bind(&resetRouterRequest); err != nil {
719 return c.JSON(http.StatusBadRequest, ErrorResponse{
720 Message: fmt.Sprintf("Bad request: %s", err.Error()),
721 })
722 }
723
724 err := httpSvc.api.ResetRouter(resetRouterRequest.Key)
725
726 if err != nil {
727 return c.JSON(http.StatusInternalServerError, ErrorResponse{
728 Message: err.Error(),
729 })
730 }
731
732 return c.NoContent(http.StatusNoContent)
733 }
734
735 func (httpSvc *HttpService) stopHandler(c echo.Context) error {
736
737 err := httpSvc.api.Stop()
738
739 if err != nil {
740 return c.JSON(http.StatusInternalServerError, ErrorResponse{
741 Message: err.Error(),
742 })
743 }
744
745 return c.NoContent(http.StatusNoContent)
746 }
747
748 func (httpSvc *HttpService) nodeConnectionInfoHandler(c echo.Context) error {
749 ctx := c.Request().Context()
750
751 info, err := httpSvc.api.GetNodeConnectionInfo(ctx)
752
753 if err != nil {
754 return c.JSON(http.StatusInternalServerError, ErrorResponse{
755 Message: err.Error(),
756 })
757 }
758
759 return c.JSON(http.StatusOK, info)
760 }
761
762 func (httpSvc *HttpService) nodeStatusHandler(c echo.Context) error {
763 ctx := c.Request().Context()
764
765 info, err := httpSvc.api.GetNodeStatus(ctx)
766
767 if err != nil {
768 return c.JSON(http.StatusInternalServerError, ErrorResponse{
769 Message: err.Error(),
770 })
771 }
772
773 return c.JSON(http.StatusOK, info)
774 }
775
776 func (httpSvc *HttpService) nodeNetworkGraphHandler(c echo.Context) error {
777 ctx := c.Request().Context()
778
779 nodeIds := strings.Split(c.QueryParam("nodeIds"), ",")
780
781 info, err := httpSvc.api.GetNetworkGraph(ctx, nodeIds)
782
783 if err != nil {
784 return c.JSON(http.StatusInternalServerError, ErrorResponse{
785 Message: err.Error(),
786 })
787 }
788
789 return c.JSON(http.StatusOK, info)
790 }
791
792 func (httpSvc *HttpService) migrateNodeStorageHandler(c echo.Context) error {
793 ctx := c.Request().Context()
794 var migrateNodeStorageRequest api.MigrateNodeStorageRequest
795 if err := c.Bind(&migrateNodeStorageRequest); err != nil {
796 return c.JSON(http.StatusBadRequest, ErrorResponse{
797 Message: fmt.Sprintf("Bad request: %s", err.Error()),
798 })
799 }
800
801 err := httpSvc.api.MigrateNodeStorage(ctx, migrateNodeStorageRequest.To)
802
803 if err != nil {
804 return c.JSON(http.StatusInternalServerError, ErrorResponse{
805 Message: err.Error(),
806 })
807 }
808
809 return c.NoContent(http.StatusNoContent)
810 }
811
812 func (httpSvc *HttpService) balancesHandler(c echo.Context) error {
813 ctx := c.Request().Context()
814
815 balances, err := httpSvc.api.GetBalances(ctx)
816
817 if err != nil {
818 return c.JSON(http.StatusInternalServerError, ErrorResponse{
819 Message: err.Error(),
820 })
821 }
822
823 return c.JSON(http.StatusOK, balances)
824 }
825
826 func (httpSvc *HttpService) sendPaymentHandler(c echo.Context) error {
827 ctx := c.Request().Context()
828
829 var payInvoiceRequest api.PayInvoiceRequest
830 if err := c.Bind(&payInvoiceRequest); err != nil {
831 return c.JSON(http.StatusBadRequest, ErrorResponse{
832 Message: fmt.Sprintf("Bad request: %s", err.Error()),
833 })
834 }
835 amountMsat := api.ResolveToMsat(payInvoiceRequest.AmountSat, payInvoiceRequest.AmountMsat, nil, payInvoiceRequest.Amount)
836
837 paymentResponse, err := httpSvc.api.SendPayment(ctx, c.Param("invoice"), amountMsat, payInvoiceRequest.Metadata, payInvoiceRequest.FromAppID)
838
839 if err != nil {
840 return c.JSON(http.StatusInternalServerError, ErrorResponse{
841 Message: err.Error(),
842 })
843 }
844
845 return c.JSON(http.StatusOK, paymentResponse)
846 }
847
848 func (httpSvc *HttpService) makeOfferHandler(c echo.Context) error {
849 ctx := c.Request().Context()
850
851 var makeOfferRequest api.MakeOfferRequest
852 if err := c.Bind(&makeOfferRequest); err != nil {
853 return c.JSON(http.StatusBadRequest, ErrorResponse{
854 Message: fmt.Sprintf("Bad request: %s", err.Error()),
855 })
856 }
857
858 offer, err := httpSvc.api.MakeOffer(ctx, makeOfferRequest.Description)
859
860 if err != nil {
861 return c.JSON(http.StatusInternalServerError, ErrorResponse{
862 Message: fmt.Sprintf("Failed to generate BOLT-12 offer: %s", err.Error()),
863 })
864 }
865
866 return c.JSON(http.StatusOK, offer)
867 }
868
869 func (httpSvc *HttpService) makeInvoiceHandler(c echo.Context) error {
870 var makeInvoiceRequest api.MakeInvoiceRequest
871 if err := c.Bind(&makeInvoiceRequest); err != nil {
872 return c.JSON(http.StatusBadRequest, ErrorResponse{
873 Message: fmt.Sprintf("Bad request: %s", err.Error()),
874 })
875 }
876
877 amountMsat := uint64(0)
878 resolvedAmountMsat := api.ResolveToMsat(makeInvoiceRequest.AmountSat, makeInvoiceRequest.AmountMsat, nil, makeInvoiceRequest.Amount)
879 if resolvedAmountMsat != nil {
880 amountMsat = *resolvedAmountMsat
881 }
882
883 invoice, err := httpSvc.api.CreateInvoice(c.Request().Context(), amountMsat, makeInvoiceRequest.Description, makeInvoiceRequest.ToAppID)
884
885 if err != nil {
886 return c.JSON(http.StatusInternalServerError, ErrorResponse{
887 Message: err.Error(),
888 })
889 }
890
891 return c.JSON(http.StatusOK, invoice)
892 }
893
894 func (httpSvc *HttpService) lookupTransactionHandler(c echo.Context) error {
895 ctx := c.Request().Context()
896
897 transaction, err := httpSvc.api.LookupInvoice(ctx, c.Param("paymentHash"))
898
899 if err != nil {
900 return c.JSON(http.StatusInternalServerError, ErrorResponse{
901 Message: err.Error(),
902 })
903 }
904
905 return c.JSON(http.StatusOK, transaction)
906 }
907
908 func (httpSvc *HttpService) setTransactionUserLabelsHandler(c echo.Context) error {
909 ctx := c.Request().Context()
910
911 var requestData api.SetTransactionUserLabelsRequest
912 if err := c.Bind(&requestData); err != nil {
913 return c.JSON(http.StatusBadRequest, ErrorResponse{
914 Message: fmt.Sprintf("Bad request: %s", err.Error()),
915 })
916 }
917
918 transactionID, err := strconv.ParseUint(c.Param("id"), 10, 64)
919 if err != nil {
920 return c.JSON(http.StatusBadRequest, ErrorResponse{
921 Message: "Invalid transaction ID",
922 })
923 }
924
925 err = httpSvc.api.SetTransactionUserLabels(ctx, uint(transactionID), requestData.Labels)
926 if err != nil {
927 return c.JSON(http.StatusInternalServerError, ErrorResponse{
928 Message: err.Error(),
929 })
930 }
931
932 return c.NoContent(http.StatusNoContent)
933 }
934
935 func (httpSvc *HttpService) listTransactionsHandler(c echo.Context) error {
936 ctx := c.Request().Context()
937
938 limit := uint64(20)
939 offset := uint64(0)
940 var appId *uint
941
942 if limitParam := c.QueryParam("limit"); limitParam != "" {
943 if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
944 limit = parsedLimit
945 }
946 }
947
948 if offsetParam := c.QueryParam("offset"); offsetParam != "" {
949 if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
950 offset = parsedOffset
951 }
952 }
953
954 if appIdParam := c.QueryParam("appId"); appIdParam != "" {
955 if parsedAppId, err := strconv.ParseUint(appIdParam, 10, 64); err == nil {
956 var unsignedAppId = uint(parsedAppId)
957 appId = &unsignedAppId
958 }
959 }
960
961 filters, err := api.ParseListTransactionsFilters(c.QueryParams())
962 if err != nil {
963 return c.JSON(http.StatusBadRequest, ErrorResponse{
964 Message: err.Error(),
965 })
966 }
967
968 transactions, err := httpSvc.api.ListTransactions(ctx, appId, limit, offset, filters)
969
970 if err != nil {
971 return c.JSON(http.StatusInternalServerError, ErrorResponse{
972 Message: err.Error(),
973 })
974 }
975
976 return c.JSON(http.StatusOK, transactions)
977 }
978
979 func (httpSvc *HttpService) listOnchainTransactionsHandler(c echo.Context) error {
980 ctx := c.Request().Context()
981
982 transactions, err := httpSvc.api.ListOnchainTransactions(ctx)
983
984 if err != nil {
985 return c.JSON(http.StatusInternalServerError, ErrorResponse{
986 Message: err.Error(),
987 })
988 }
989
990 return c.JSON(http.StatusOK, transactions)
991 }
992
993 func (httpSvc *HttpService) walletSyncHandler(c echo.Context) error {
994 httpSvc.api.SyncWallet()
995
996 return c.NoContent(http.StatusNoContent)
997 }
998
999 func (httpSvc *HttpService) mempoolApiHandler(c echo.Context) error {
1000 endpoint := c.QueryParam("endpoint")
1001 if endpoint == "" {
1002 return c.JSON(http.StatusBadRequest, ErrorResponse{
1003 Message: "Invalid pubkey parameter",
1004 })
1005 }
1006
1007 response, err := httpSvc.api.RequestMempoolApi(c.Request().Context(), endpoint)
1008 if err != nil {
1009 logger.Logger.WithField("endpoint", endpoint).WithError(err).Error("Failed to request mempool API")
1010 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1011 Message: fmt.Sprintf("Failed to request mempool API: %s", err.Error()),
1012 })
1013 }
1014
1015 return c.JSON(http.StatusOK, response)
1016 }
1017
1018 func (httpSvc *HttpService) capabilitiesHandler(c echo.Context) error {
1019 response, err := httpSvc.api.GetWalletCapabilities(c.Request().Context())
1020 if err != nil {
1021 logger.Logger.WithError(err).Error("Failed to request wallet capabilities")
1022 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1023 Message: fmt.Sprintf("Failed to request wallet capabilities: %s", err.Error()),
1024 })
1025 }
1026
1027 return c.JSON(http.StatusOK, response)
1028 }
1029
1030 func (httpSvc *HttpService) listPeers(c echo.Context) error {
1031 peers, err := httpSvc.api.ListPeers(c.Request().Context())
1032 if err != nil {
1033 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1034 Message: fmt.Sprintf("Failed to list peers: %s", err.Error()),
1035 })
1036 }
1037
1038 return c.JSON(http.StatusOK, peers)
1039 }
1040
1041 func (httpSvc *HttpService) connectPeerHandler(c echo.Context) error {
1042 ctx := c.Request().Context()
1043
1044 var connectPeerRequest api.ConnectPeerRequest
1045 if err := c.Bind(&connectPeerRequest); err != nil {
1046 return c.JSON(http.StatusBadRequest, ErrorResponse{
1047 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1048 })
1049 }
1050
1051 err := httpSvc.api.ConnectPeer(ctx, &connectPeerRequest)
1052
1053 if err != nil {
1054 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1055 Message: fmt.Sprintf("Failed to connect peer: %s", err.Error()),
1056 })
1057 }
1058
1059 return c.NoContent(http.StatusNoContent)
1060 }
1061
1062 func (httpSvc *HttpService) openChannelHandler(c echo.Context) error {
1063 ctx := c.Request().Context()
1064
1065 var openChannelRequest api.OpenChannelRequest
1066 if err := c.Bind(&openChannelRequest); err != nil {
1067 return c.JSON(http.StatusBadRequest, ErrorResponse{
1068 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1069 })
1070 }
1071
1072 openChannelResponse, err := httpSvc.api.OpenChannel(ctx, &openChannelRequest)
1073
1074 if err != nil {
1075 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1076 Message: fmt.Sprintf("Failed to open channel: %s", err.Error()),
1077 })
1078 }
1079
1080 return c.JSON(http.StatusOK, openChannelResponse)
1081 }
1082
1083 func (httpSvc *HttpService) rebalanceChannelHandler(c echo.Context) error {
1084 ctx := c.Request().Context()
1085
1086 var rebalanceChannelRequest api.RebalanceChannelRequest
1087 if err := c.Bind(&rebalanceChannelRequest); err != nil {
1088 return c.JSON(http.StatusBadRequest, ErrorResponse{
1089 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1090 })
1091 }
1092
1093 rebalanceChannelResponse, err := httpSvc.api.RebalanceChannel(ctx, &rebalanceChannelRequest)
1094
1095 if err != nil {
1096 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1097 Message: fmt.Sprintf("Failed to rebalance channel: %s", err.Error()),
1098 })
1099 }
1100
1101 return c.JSON(http.StatusOK, rebalanceChannelResponse)
1102 }
1103
1104 func (httpSvc *HttpService) disconnectPeerHandler(c echo.Context) error {
1105 ctx := c.Request().Context()
1106
1107 err := httpSvc.api.DisconnectPeer(ctx, c.Param("peerId"))
1108
1109 if err != nil {
1110 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1111 Message: fmt.Sprintf("Failed to disconnect peer: %s", err.Error()),
1112 })
1113 }
1114
1115 return c.NoContent(http.StatusNoContent)
1116 }
1117
1118 func (httpSvc *HttpService) closeChannelHandler(c echo.Context) error {
1119 ctx := c.Request().Context()
1120
1121 closeChannelResponse, err := httpSvc.api.CloseChannel(ctx, c.Param("peerId"), c.Param("channelId"), c.QueryParam("force") == "true")
1122
1123 if err != nil {
1124 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1125 Message: fmt.Sprintf("Failed to close channel: %s", err.Error()),
1126 })
1127 }
1128
1129 return c.JSON(http.StatusOK, closeChannelResponse)
1130 }
1131
1132 func (httpSvc *HttpService) updateChannelHandler(c echo.Context) error {
1133 ctx := c.Request().Context()
1134
1135 var updateChannelRequest api.UpdateChannelRequest
1136 if err := c.Bind(&updateChannelRequest); err != nil {
1137 return c.JSON(http.StatusBadRequest, ErrorResponse{
1138 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1139 })
1140 }
1141
1142 updateChannelRequest.NodeId = c.Param("peerId")
1143 updateChannelRequest.ChannelId = c.Param("channelId")
1144
1145 err := httpSvc.api.UpdateChannel(ctx, &updateChannelRequest)
1146
1147 if err != nil {
1148 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1149 Message: fmt.Sprintf("Failed to update channel: %s", err.Error()),
1150 })
1151 }
1152
1153 return c.NoContent(http.StatusNoContent)
1154 }
1155
1156 func (httpSvc *HttpService) newInstantChannelInvoiceHandler(c echo.Context) error {
1157 ctx := c.Request().Context()
1158
1159 var newWrappedInvoiceRequest api.LSPOrderRequest
1160 if err := c.Bind(&newWrappedInvoiceRequest); err != nil {
1161 return c.JSON(http.StatusBadRequest, ErrorResponse{
1162 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1163 })
1164 }
1165
1166 newLSPOrderResponse, err := httpSvc.api.RequestLSPOrder(ctx, &newWrappedInvoiceRequest)
1167
1168 if err != nil {
1169 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1170 Message: fmt.Sprintf("Failed to request new channel order from LSP: %s", err.Error()),
1171 })
1172 }
1173
1174 return c.JSON(http.StatusOK, newLSPOrderResponse)
1175 }
1176
1177 func (httpSvc *HttpService) onchainAddressHandler(c echo.Context) error {
1178 ctx := c.Request().Context()
1179
1180 address, err := httpSvc.api.GetUnusedOnchainAddress(ctx)
1181
1182 if err != nil {
1183 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1184 Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
1185 })
1186 }
1187
1188 return c.JSON(http.StatusOK, address)
1189 }
1190
1191 func (httpSvc *HttpService) newOnchainAddressHandler(c echo.Context) error {
1192 ctx := c.Request().Context()
1193
1194 address, err := httpSvc.api.GetNewOnchainAddress(ctx)
1195
1196 if err != nil {
1197 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1198 Message: fmt.Sprintf("Failed to request new onchain address: %s", err.Error()),
1199 })
1200 }
1201
1202 return c.JSON(http.StatusOK, address)
1203 }
1204
1205 func (httpSvc *HttpService) redeemOnchainFundsHandler(c echo.Context) error {
1206 ctx := c.Request().Context()
1207
1208 var redeemOnchainFundsRequest api.RedeemOnchainFundsRequest
1209 if err := c.Bind(&redeemOnchainFundsRequest); err != nil {
1210 return c.JSON(http.StatusBadRequest, ErrorResponse{
1211 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1212 })
1213 }
1214
1215 amountSat := uint64(0)
1216 resolvedAmountSat := api.ResolveToSat(redeemOnchainFundsRequest.AmountSat, nil, redeemOnchainFundsRequest.Amount, nil)
1217 if resolvedAmountSat != nil {
1218 amountSat = *resolvedAmountSat
1219 }
1220
1221 redeemOnchainFundsResponse, err := httpSvc.api.RedeemOnchainFunds(ctx, redeemOnchainFundsRequest.ToAddress, amountSat, redeemOnchainFundsRequest.FeeRate, redeemOnchainFundsRequest.SendAll)
1222
1223 if err != nil {
1224 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1225 Message: fmt.Sprintf("Failed to redeem onchain funds: %s", err.Error()),
1226 })
1227 }
1228
1229 return c.JSON(http.StatusOK, redeemOnchainFundsResponse)
1230 }
1231
1232 func (httpSvc *HttpService) signMessageHandler(c echo.Context) error {
1233 ctx := c.Request().Context()
1234
1235 var signMessageRequest api.SignMessageRequest
1236 if err := c.Bind(&signMessageRequest); err != nil {
1237 return c.JSON(http.StatusBadRequest, ErrorResponse{
1238 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1239 })
1240 }
1241
1242 signMessageResponse, err := httpSvc.api.SignMessage(ctx, signMessageRequest.Message)
1243
1244 if err != nil {
1245 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1246 Message: fmt.Sprintf("Failed to sign message: %s", err.Error()),
1247 })
1248 }
1249 return c.JSON(http.StatusOK, signMessageResponse)
1250 }
1251
1252 func (httpSvc *HttpService) appsListHandler(c echo.Context) error {
1253 limit := uint64(0)
1254 offset := uint64(0)
1255
1256 if limitParam := c.QueryParam("limit"); limitParam != "" {
1257 if parsedLimit, err := strconv.ParseUint(limitParam, 10, 64); err == nil {
1258 limit = parsedLimit
1259 }
1260 }
1261
1262 if offsetParam := c.QueryParam("offset"); offsetParam != "" {
1263 if parsedOffset, err := strconv.ParseUint(offsetParam, 10, 64); err == nil {
1264 offset = parsedOffset
1265 }
1266 }
1267
1268 filtersJSON := c.QueryParam("filters")
1269 var filters api.ListAppsFilters
1270 if filtersJSON != "" {
1271 err := json.Unmarshal([]byte(filtersJSON), &filters)
1272 if err != nil {
1273 logger.Logger.WithError(err).WithFields(logrus.Fields{
1274 "filters": filtersJSON,
1275 }).Error("Failed to deserialize app filters")
1276 return err
1277 }
1278 }
1279
1280 orderBy := c.QueryParam("order_by")
1281
1282 apps, err := httpSvc.api.ListApps(limit, offset, filters, orderBy)
1283
1284 if err != nil {
1285 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1286 Message: err.Error(),
1287 })
1288 }
1289
1290 return c.JSON(http.StatusOK, apps)
1291 }
1292
1293 func (httpSvc *HttpService) appsShowByPubkeyHandler(c echo.Context) error {
1294 dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1295
1296 if dbApp == nil {
1297 return c.JSON(http.StatusNotFound, ErrorResponse{
1298 Message: "App not found",
1299 })
1300 }
1301
1302 response, err := httpSvc.api.GetApp(dbApp)
1303 if err != nil {
1304 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1305 Message: err.Error(),
1306 })
1307 }
1308
1309 return c.JSON(http.StatusOK, response)
1310 }
1311
1312 func (httpSvc *HttpService) appsShowHandler(c echo.Context) error {
1313 appIdStr := c.Param("id")
1314 if appIdStr == "" {
1315 return c.JSON(http.StatusBadRequest, ErrorResponse{
1316 Message: "App ID is required",
1317 })
1318 }
1319
1320 appId, err := strconv.ParseUint(appIdStr, 10, 64)
1321 if err != nil {
1322 return c.JSON(http.StatusBadRequest, ErrorResponse{
1323 Message: "Invalid App ID",
1324 })
1325 }
1326
1327 dbApp := httpSvc.appsSvc.GetAppById(uint(appId))
1328
1329 if dbApp == nil {
1330 return c.JSON(http.StatusNotFound, ErrorResponse{
1331 Message: "App not found",
1332 })
1333 }
1334
1335 response, err := httpSvc.api.GetApp(dbApp)
1336 if err != nil {
1337 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1338 Message: err.Error(),
1339 })
1340 }
1341
1342 return c.JSON(http.StatusOK, response)
1343 }
1344
1345 func (httpSvc *HttpService) appsUpdateHandler(c echo.Context) error {
1346 var requestData api.UpdateAppRequest
1347 if err := c.Bind(&requestData); err != nil {
1348 return c.JSON(http.StatusBadRequest, ErrorResponse{
1349 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1350 })
1351 }
1352
1353 dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1354
1355 if dbApp == nil {
1356 return c.JSON(http.StatusNotFound, ErrorResponse{
1357 Message: "App not found",
1358 })
1359 }
1360
1361 err := httpSvc.api.UpdateApp(dbApp, &requestData)
1362
1363 if err != nil {
1364 logger.Logger.WithError(err).Error("Failed to update app")
1365 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1366 Message: fmt.Sprintf("Failed to update app: %v", err),
1367 })
1368 }
1369
1370 return c.NoContent(http.StatusNoContent)
1371 }
1372
1373 func (httpSvc *HttpService) transfersHandler(c echo.Context) error {
1374 var requestData api.TransferRequest
1375 if err := c.Bind(&requestData); err != nil {
1376 return c.JSON(http.StatusBadRequest, ErrorResponse{
1377 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1378 })
1379 }
1380
1381 amountMsat := uint64(0)
1382 resolvedAmountMsat := api.ResolveToMsat(requestData.AmountSat, requestData.AmountMsat, nil, nil)
1383 if resolvedAmountMsat != nil {
1384 amountMsat = *resolvedAmountMsat
1385 }
1386
1387 err := httpSvc.api.Transfer(c.Request().Context(), requestData.FromAppId, requestData.ToAppId, amountMsat, requestData.Description)
1388
1389 if err != nil {
1390 logger.Logger.WithError(err).Error("Failed to transfer funds")
1391 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1392 Message: fmt.Sprintf("Failed to transfer funds: %v", err),
1393 })
1394 }
1395
1396 return c.NoContent(http.StatusNoContent)
1397 }
1398
1399 func (httpSvc *HttpService) appsDeleteHandler(c echo.Context) error {
1400 dbApp := httpSvc.appsSvc.GetAppByPubkey(c.Param("pubkey"))
1401 if dbApp == nil {
1402 return c.JSON(http.StatusNotFound, ErrorResponse{
1403 Message: "App not found",
1404 })
1405 }
1406
1407 if err := httpSvc.api.DeleteApp(dbApp); err != nil {
1408 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1409 Message: "Failed to delete app",
1410 })
1411 }
1412 return c.NoContent(http.StatusNoContent)
1413 }
1414
1415 func (httpSvc *HttpService) appsCreateHandler(c echo.Context) error {
1416 var requestData api.CreateAppRequest
1417 if err := c.Bind(&requestData); err != nil {
1418 return c.JSON(http.StatusBadRequest, ErrorResponse{
1419 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1420 })
1421 }
1422
1423 responseBody, err := httpSvc.api.CreateApp(&requestData)
1424
1425 if err != nil {
1426 logger.Logger.WithField("appName", requestData.Name).WithError(err).Error("Failed to save app")
1427 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1428 Message: fmt.Sprintf("Failed to save app: %v", err),
1429 })
1430 }
1431
1432 return c.JSON(http.StatusOK, responseBody)
1433 }
1434
1435 func (httpSvc *HttpService) lightningAddressesCreateHandler(c echo.Context) error {
1436 var requestData api.CreateLightningAddressRequest
1437 if err := c.Bind(&requestData); err != nil {
1438 return c.JSON(http.StatusBadRequest, ErrorResponse{
1439 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1440 })
1441 }
1442
1443 err := httpSvc.api.CreateLightningAddress(c.Request().Context(), &requestData)
1444
1445 if err != nil {
1446 logger.Logger.WithField("request", requestData).WithError(err).Error("Failed to create lightning address")
1447 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1448 Message: err.Error(),
1449 })
1450 }
1451
1452 return c.NoContent(http.StatusNoContent)
1453 }
1454
1455 func (httpSvc *HttpService) lightningAddressesDeleteHandler(c echo.Context) error {
1456 appIdStr := c.Param("appId")
1457 if appIdStr == "" {
1458 return c.JSON(http.StatusBadRequest, ErrorResponse{
1459 Message: "App ID is required",
1460 })
1461 }
1462
1463 appId, err := strconv.ParseUint(appIdStr, 10, 64)
1464 if err != nil {
1465 return c.JSON(http.StatusBadRequest, ErrorResponse{
1466 Message: "Invalid App ID",
1467 })
1468 }
1469
1470 err = httpSvc.api.DeleteLightningAddress(c.Request().Context(), uint(appId))
1471 if err != nil {
1472 logger.Logger.WithField("appId", appId).WithError(err).Error("Failed to delete lightning address")
1473 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1474 Message: err.Error(),
1475 })
1476 }
1477
1478 return c.NoContent(http.StatusNoContent)
1479 }
1480
1481 func (httpSvc *HttpService) setupHandler(c echo.Context) error {
1482 var setupRequest api.SetupRequest
1483 if err := c.Bind(&setupRequest); err != nil {
1484 return c.JSON(http.StatusBadRequest, ErrorResponse{
1485 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1486 })
1487 }
1488
1489 err := httpSvc.api.Setup(c.Request().Context(), &setupRequest)
1490 if err != nil {
1491 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1492 Message: fmt.Sprintf("Failed to setup node: %s", err.Error()),
1493 })
1494 }
1495
1496 return c.NoContent(http.StatusNoContent)
1497 }
1498
1499 func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error {
1500 var getLogRequest api.GetLogOutputRequest
1501 if err := c.Bind(&getLogRequest); err != nil {
1502 return c.JSON(http.StatusBadRequest, ErrorResponse{
1503 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1504 })
1505 }
1506
1507 logType := c.Param("type")
1508 if logType != api.LogTypeNode && logType != api.LogTypeApp {
1509 return c.JSON(http.StatusBadRequest, ErrorResponse{
1510 Message: fmt.Sprintf("Invalid log type parameter: '%s'", logType),
1511 })
1512 }
1513
1514 getLogResponse, err := httpSvc.api.GetLogOutput(c.Request().Context(), logType, &getLogRequest)
1515 if err != nil {
1516 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1517 Message: fmt.Sprintf("Failed to get log output: %v", err),
1518 })
1519 }
1520
1521 return c.JSON(http.StatusOK, getLogResponse)
1522 }
1523
1524 func (httpSvc *HttpService) getCustomNodeCommandsHandler(c echo.Context) error {
1525 nodeCommandsResponse, err := httpSvc.api.GetCustomNodeCommands()
1526 if err != nil {
1527 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1528 Message: fmt.Sprintf("Failed to get node commands: %v", err),
1529 })
1530 }
1531
1532 return c.JSON(http.StatusOK, nodeCommandsResponse)
1533 }
1534
1535 func (httpSvc *HttpService) execCustomNodeCommandHandler(c echo.Context) error {
1536 var execCommandRequest api.ExecuteCustomNodeCommandRequest
1537 if err := c.Bind(&execCommandRequest); err != nil {
1538 return c.JSON(http.StatusBadRequest, ErrorResponse{
1539 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1540 })
1541 }
1542
1543 execCommandResponse, err := httpSvc.api.ExecuteCustomNodeCommand(c.Request().Context(), execCommandRequest.Command)
1544 if err != nil {
1545 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1546 Message: fmt.Sprintf("Failed to execute command: %v", err),
1547 })
1548 }
1549
1550 return c.JSON(http.StatusOK, execCommandResponse)
1551 }
1552
1553 func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
1554 c.SetCookie(&http.Cookie{
1555 Name: nip07SessionCookie,
1556 Value: "",
1557 Path: "/",
1558 HttpOnly: true,
1559 Secure: c.IsTLS() || c.Request().Header.Get("X-Forwarded-Proto") == "https",
1560 SameSite: http.SameSiteLaxMode,
1561 MaxAge: -1,
1562 Expires: time.Unix(0, 0),
1563 })
1564
1565 redirectUrl := httpSvc.cfg.GetEnv().GetBaseFrontendUrl()
1566 if redirectUrl == "" {
1567 redirectUrl = "/"
1568 }
1569
1570 return c.Redirect(http.StatusFound, redirectUrl)
1571 }
1572
1573 func (httpSvc *HttpService) createBackupHandler(c echo.Context) error {
1574 var backupRequest api.BasicBackupRequest
1575 if err := c.Bind(&backupRequest); err != nil {
1576 return c.JSON(http.StatusBadRequest, ErrorResponse{
1577 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1578 })
1579 }
1580
1581 if !httpSvc.cfg.CheckUnlockPassword(backupRequest.UnlockPassword) {
1582 return c.JSON(http.StatusUnauthorized, ErrorResponse{
1583 Message: "Invalid password",
1584 })
1585 }
1586
1587 var buffer bytes.Buffer
1588 err := httpSvc.api.CreateBackup(backupRequest.UnlockPassword, &buffer)
1589 if err != nil {
1590 return c.String(500, fmt.Sprintf("Failed to create backup: %v", err))
1591 }
1592
1593 c.Response().Header().Set("Content-Type", "application/octet-stream")
1594 c.Response().Header().Set("Content-Disposition", "attachment; filename=albyhub.bkp")
1595 c.Response().WriteHeader(http.StatusOK)
1596 c.Response().Write(buffer.Bytes())
1597 return nil
1598 }
1599
1600 func (httpSvc *HttpService) restoreBackupHandler(c echo.Context) error {
1601 info, err := httpSvc.api.GetInfo(c.Request().Context())
1602 if err != nil {
1603 return err
1604 }
1605 if info.SetupCompleted {
1606 return errors.New("setup already completed")
1607 }
1608
1609 password := c.FormValue("unlockPassword")
1610
1611 fileHeader, err := c.FormFile("backup")
1612 if err != nil {
1613 return c.JSON(http.StatusBadRequest, ErrorResponse{
1614 Message: fmt.Sprintf("Failed to get backup file header: %v", err),
1615 })
1616 }
1617
1618 file, err := fileHeader.Open()
1619 if err != nil {
1620 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1621 Message: fmt.Sprintf("Failed to open backup file: %v", err),
1622 })
1623 }
1624 defer file.Close()
1625
1626 err = httpSvc.api.RestoreBackup(password, file)
1627 if err != nil {
1628 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1629 Message: fmt.Sprintf("Failed to restore backup: %v", err),
1630 })
1631 }
1632
1633 return c.NoContent(http.StatusNoContent)
1634 }
1635
1636 func (httpSvc *HttpService) healthHandler(c echo.Context) error {
1637 healthResponse, err := httpSvc.api.Health(c.Request().Context())
1638 if err != nil {
1639 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1640 Message: fmt.Sprintf("Failed to check node health: %v", err),
1641 })
1642 }
1643
1644 return c.JSON(http.StatusOK, healthResponse)
1645 }
1646
1647 func (httpSvc *HttpService) listSwapsHandler(c echo.Context) error {
1648 swaps, err := httpSvc.api.ListSwaps()
1649
1650 if err != nil {
1651 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1652 Message: err.Error(),
1653 })
1654 }
1655
1656 return c.JSON(http.StatusOK, swaps)
1657 }
1658
1659 func (httpSvc *HttpService) lookupSwapHandler(c echo.Context) error {
1660 swap, err := httpSvc.api.LookupSwap(c.Param("swapId"))
1661 if err != nil {
1662 return c.JSON(http.StatusNotFound, ErrorResponse{
1663 Message: "App not found",
1664 })
1665 }
1666
1667 return c.JSON(http.StatusOK, swap)
1668 }
1669
1670 func (httpSvc *HttpService) getSwapOutInfoHandler(c echo.Context) error {
1671 swapOutFeesResponse, err := httpSvc.api.GetSwapOutInfo()
1672 if err != nil {
1673 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1674 Message: fmt.Sprintf("Failed to get swap out info: %v", err),
1675 })
1676 }
1677
1678 return c.JSON(http.StatusOK, swapOutFeesResponse)
1679 }
1680
1681 func (httpSvc *HttpService) getSwapInInfoHandler(c echo.Context) error {
1682 swapOutFeesResponse, err := httpSvc.api.GetSwapInInfo()
1683 if err != nil {
1684 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1685 Message: fmt.Sprintf("Failed to get swap in info: %v", err),
1686 })
1687 }
1688
1689 return c.JSON(http.StatusOK, swapOutFeesResponse)
1690 }
1691
1692 func (httpSvc *HttpService) initiateSwapOutHandler(c echo.Context) error {
1693 var initiateSwapOutRequest api.InitiateSwapRequest
1694 if err := c.Bind(&initiateSwapOutRequest); err != nil {
1695 return c.JSON(http.StatusBadRequest, ErrorResponse{
1696 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1697 })
1698 }
1699
1700 swapOutResponse, err := httpSvc.api.InitiateSwapOut(c.Request().Context(), &initiateSwapOutRequest)
1701 if err != nil {
1702 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1703 Message: fmt.Sprintf("Failed to initiate swap out: %v", err),
1704 })
1705 }
1706
1707 return c.JSON(http.StatusOK, swapOutResponse)
1708 }
1709
1710 func (httpSvc *HttpService) initiateSwapInHandler(c echo.Context) error {
1711 var initiateSwapInRequest api.InitiateSwapRequest
1712 if err := c.Bind(&initiateSwapInRequest); err != nil {
1713 return c.JSON(http.StatusBadRequest, ErrorResponse{
1714 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1715 })
1716 }
1717
1718 txId, err := httpSvc.api.InitiateSwapIn(c.Request().Context(), &initiateSwapInRequest)
1719 if err != nil {
1720 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1721 Message: fmt.Sprintf("Failed to initiate swap in: %v", err),
1722 })
1723 }
1724
1725 return c.JSON(http.StatusOK, txId)
1726 }
1727
1728 func (httpSvc *HttpService) refundSwapHandler(c echo.Context) error {
1729 var refundSwapInRequest api.RefundSwapRequest
1730 if err := c.Bind(&refundSwapInRequest); err != nil {
1731 return c.JSON(http.StatusBadRequest, ErrorResponse{
1732 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1733 })
1734 }
1735
1736 err := httpSvc.api.RefundSwap(&refundSwapInRequest)
1737 if err != nil {
1738 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1739 Message: err.Error(),
1740 })
1741 }
1742
1743 return c.NoContent(http.StatusNoContent)
1744 }
1745
1746 func (httpSvc *HttpService) swapMnemonicHandler(c echo.Context) error {
1747 mnemonic := httpSvc.api.GetSwapMnemonic()
1748 return c.JSON(http.StatusOK, mnemonic)
1749 }
1750
1751 func (httpSvc *HttpService) getAutoSwapConfigHandler(c echo.Context) error {
1752 getAutoSwapConfigResponse, err := httpSvc.api.GetAutoSwapConfig()
1753 if err != nil {
1754 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1755 Message: fmt.Sprintf("Failed to get swap settings: %v", err),
1756 })
1757 }
1758
1759 return c.JSON(http.StatusOK, getAutoSwapConfigResponse)
1760 }
1761
1762 func (httpSvc *HttpService) enableAutoSwapOutHandler(c echo.Context) error {
1763 var enableAutoSwapRequest api.EnableAutoSwapRequest
1764 if err := c.Bind(&enableAutoSwapRequest); err != nil {
1765 return c.JSON(http.StatusBadRequest, ErrorResponse{
1766 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1767 })
1768 }
1769
1770 err := httpSvc.api.EnableAutoSwapOut(c.Request().Context(), &enableAutoSwapRequest)
1771 if err != nil {
1772 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1773 Message: fmt.Sprintf("Failed to save swap settings: %v", err),
1774 })
1775 }
1776
1777 return c.NoContent(http.StatusNoContent)
1778 }
1779
1780 func (httpSvc *HttpService) disableAutoSwapOutHandler(c echo.Context) error {
1781 err := httpSvc.api.DisableAutoSwap()
1782
1783 if err != nil {
1784 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1785 Message: err.Error(),
1786 })
1787 }
1788
1789 return c.NoContent(http.StatusNoContent)
1790 }
1791
1792 func (httpSvc *HttpService) setNodeAliasHandler(c echo.Context) error {
1793 var setNodeAliasRequest api.SetNodeAliasRequest
1794 if err := c.Bind(&setNodeAliasRequest); err != nil {
1795 return c.JSON(http.StatusBadRequest, ErrorResponse{
1796 Message: fmt.Sprintf("Bad request: %s", err.Error()),
1797 })
1798 }
1799
1800 err := httpSvc.api.SetNodeAlias(setNodeAliasRequest.NodeAlias)
1801 if err != nil {
1802 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1803 Message: fmt.Sprintf("Failed to set node alias: %s", err.Error()),
1804 })
1805 }
1806
1807 return c.NoContent(http.StatusNoContent)
1808 }
1809
1810 func (httpSvc *HttpService) forwardsHandler(c echo.Context) error {
1811 forwards, err := httpSvc.api.GetForwards()
1812 if err != nil {
1813 return c.JSON(http.StatusInternalServerError, ErrorResponse{
1814 Message: fmt.Sprintf("Failed to get forwards: %s", err.Error()),
1815 })
1816 }
1817
1818 return c.JSON(http.StatusOK, forwards)
1819 }
1820