diff --git a/alby/alby_oauth_service.go b/alby/alby_oauth_service.go index 9b58c8eb..4e7e7887 100644 --- a/alby/alby_oauth_service.go +++ b/alby/alby_oauth_service.go @@ -395,6 +395,15 @@ func (svc *albyOAuthService) GetAuthUrl() string { return svc.oauthConf.AuthCodeURL("unused") } +func (svc *albyOAuthService) UnlinkAccount(ctx context.Context) error { + svc.cfg.SetUpdate(userIdentifierKey, "", "") + svc.cfg.SetUpdate(accessTokenKey, "", "") + svc.cfg.SetUpdate(accessTokenExpiryKey, "", "") + svc.cfg.SetUpdate(refreshTokenKey, "", "") + + return nil +} + func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.LNClient, budget uint64, renewal string) error { connectionPubkey, err := svc.createAlbyAccountNWCNode(ctx) if err != nil { diff --git a/alby/models.go b/alby/models.go index a1a1949e..2edf00cd 100644 --- a/alby/models.go +++ b/alby/models.go @@ -19,6 +19,7 @@ type AlbyOAuthService interface { GetMe(ctx context.Context) (*AlbyMe, error) SendPayment(ctx context.Context, invoice string) error DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error + UnlinkAccount(ctx context.Context) error RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error) } diff --git a/frontend/src/components/layouts/SettingsLayout.tsx b/frontend/src/components/layouts/SettingsLayout.tsx index 64cb2daf..d5cf2dfe 100644 --- a/frontend/src/components/layouts/SettingsLayout.tsx +++ b/frontend/src/components/layouts/SettingsLayout.tsx @@ -108,6 +108,7 @@ export default function SettingsLayout() { {hasNodeBackup && ( Migrate Node )} + Alby Account Debug Tools diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 919eff47..14d38298 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -34,6 +34,7 @@ import BuyBitcoin from "src/screens/onchain/BuyBitcoin"; import DepositBitcoin from "src/screens/onchain/DepositBitcoin"; import ConnectPeer from "src/screens/peers/ConnectPeer"; import Peers from "src/screens/peers/Peers"; +import { AlbyAccount } from "src/screens/settings/AlbyAccount"; import { ChangeUnlockPassword } from "src/screens/settings/ChangeUnlockPassword"; import DebugTools from "src/screens/settings/DebugTools"; import Settings from "src/screens/settings/Settings"; @@ -129,6 +130,10 @@ const routes = [ path: "node-backup", element: , }, + { + path: "alby-account", + element: , + }, ], }, ], diff --git a/frontend/src/screens/settings/AlbyAccount.tsx b/frontend/src/screens/settings/AlbyAccount.tsx new file mode 100644 index 00000000..74e4a990 --- /dev/null +++ b/frontend/src/screens/settings/AlbyAccount.tsx @@ -0,0 +1,93 @@ +import { ExitIcon } from "@radix-ui/react-icons"; +import { ExternalLinkIcon } from "lucide-react"; + +import ExternalLink from "src/components/ExternalLink"; +import SettingsHeader from "src/components/SettingsHeader"; +import { + Card, + CardDescription, + CardHeader, + CardTitle, +} from "src/components/ui/card"; +import { useToast } from "src/components/ui/use-toast"; +import { useCSRF } from "src/hooks/useCSRF"; +import { useInfo } from "src/hooks/useInfo"; +import { request } from "src/utils/request"; + +export function AlbyAccount() { + const { data: csrf } = useCSRF(); + const { toast } = useToast(); + const { mutate: refetchInfo } = useInfo(); + + const unlink = async () => { + if ( + !confirm( + "Please log out at https://getalby.com, then click ok to continue." + ) + ) { + return; + } + + if ( + !confirm("Are you sure you want to change the Alby Account for your hub?") + ) { + return; + } + + try { + if (!csrf) { + throw new Error("No CSRF token"); + } + await request("/api/alby/unlink-account", { + method: "POST", + headers: { + "X-CSRF-Token": csrf, + "Content-Type": "application/json", + }, + }); + await refetchInfo(); + toast({ + title: "Alby Account Unlinked", + description: "Please login with another Alby Account", + }); + } catch (error) { + toast({ + title: "Unlink account failed", + description: (error as Error).message, + variant: "destructive", + }); + } + }; + + return ( + <> + + + + + Your Alby Account + + Manage your Alby Account + Settings + + + + + + + Change Alby Account + + Link your Hub to a different Alby + Account + + + + + ); +} diff --git a/http/alby_http_service.go b/http/alby_http_service.go index cac3979b..92e51627 100644 --- a/http/alby_http_service.go +++ b/http/alby_http_service.go @@ -33,6 +33,7 @@ func (albyHttpSvc *AlbyHttpService) RegisterSharedRoutes(e *echo.Echo, authMiddl e.POST("/api/alby/drain", albyHttpSvc.albyDrainHandler, authMiddleware) e.POST("/api/alby/link-account", albyHttpSvc.albyLinkAccountHandler, authMiddleware) e.POST("/api/alby/auto-channel", albyHttpSvc.autoChannelHandler, authMiddleware) + e.POST("/api/alby/unlink-account", albyHttpSvc.unlinkHandler, authMiddleware) } func (albyHttpSvc *AlbyHttpService) autoChannelHandler(c echo.Context) error { @@ -49,13 +50,27 @@ func (albyHttpSvc *AlbyHttpService) autoChannelHandler(c echo.Context) error { if err != nil { return c.JSON(http.StatusInternalServerError, ErrorResponse{ - Message: fmt.Sprintf("Failed to request wrapped invoice: %s", err.Error()), + 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 request wrapped invoice: %s", err.Error()), + }) + } + + return c.NoContent(http.StatusNoContent) +} + func (albyHttpSvc *AlbyHttpService) albyCallbackHandler(c echo.Context) error { code := c.QueryParam("code") diff --git a/wails/wails_handlers.go b/wails/wails_handlers.go index ddecc443..9b233268 100644 --- a/wails/wails_handlers.go +++ b/wails/wails_handlers.go @@ -281,6 +281,12 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string return WailsRequestRouterResponse{Body: nil, Error: err.Error()} } return WailsRequestRouterResponse{Body: nil, Error: ""} + case "/api/alby/unlink-account": + err := app.svc.GetAlbyOAuthSvc().UnlinkAccount(ctx) + if err != nil { + return WailsRequestRouterResponse{Body: nil, Error: err.Error()} + } + return WailsRequestRouterResponse{Body: nil, Error: ""} case "/api/alby/pay": payRequest := &alby.AlbyPayRequest{} err := json.Unmarshal([]byte(body), payRequest)