Skip to content

Commit

Permalink
Make Alby Account Optional (#639)
Browse files Browse the repository at this point in the history
* feat: optional Alby Account (WIP)

* fix: onboarding checklist loading check

* fix: channel suggestions without alby account

* feat: add illustrations to manual flows

* fix: show alby hub version with no alby account connected

* chore: minor ui improvements

* fix: re-enable auth redirects if user has an alby identifier, remove unused onboarding page

* chore: rename page to connect alby account

* fix: alert dialog copy

* fix: button sizes

* feat: use new alby info endpoint for latest alby hub version

* chore: remove unexpected comma from copy

* chore: move alby api URLs to the alby oauth service

* chore: improve connect alby account copy and spacing

* chore: minor ui improvements to app layout and connect alby account pages

* chore: improve check for alby automatic auth on setup finish

---------

Co-authored-by: René Aaron <[email protected]>
  • Loading branch information
rolznz and reneaaron authored Sep 12, 2024
1 parent 80625ed commit 63cd230
Show file tree
Hide file tree
Showing 27 changed files with 440 additions and 196 deletions.
78 changes: 60 additions & 18 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ const (
lightningAddressKey = "AlbyLightningAddress"
)

const (
albyOAuthAPIURL = "https://api.getalby.com"
albyInternalAPIURL = "https://getalby.com/api"
albyOAuthAuthUrl = "https://getalby.com/oauth"
)

const ALBY_ACCOUNT_APP_NAME = "getalby.com"

func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPublisher events.EventPublisher) *albyOAuthService {
Expand All @@ -57,8 +63,8 @@ func NewAlbyOAuthService(db *gorm.DB, cfg config.Config, keys keys.Keys, eventPu
ClientSecret: cfg.GetEnv().AlbyClientSecret,
Scopes: []string{"account:read", "balance:read", "payments:send"},
Endpoint: oauth2.Endpoint{
TokenURL: cfg.GetEnv().AlbyAPIURL + "/oauth/token",
AuthURL: cfg.GetEnv().AlbyOAuthAuthUrl,
TokenURL: albyOAuthAPIURL + "/oauth/token",
AuthURL: albyOAuthAuthUrl,
AuthStyle: 2, // use HTTP Basic Authorization https://pkg.go.dev/golang.org/x/oauth2#AuthStyle
},
}
Expand Down Expand Up @@ -212,6 +218,48 @@ func (svc *albyOAuthService) fetchUserToken(ctx context.Context) (*oauth2.Token,
return newToken, nil
}

func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
client := &http.Client{Timeout: 10 * time.Second}

req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/info", albyInternalAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to alby info endpoint")
return nil, err
}

setDefaultRequestHeaders(req)

res, err := client.Do(req)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch /info")
return nil, err
}

type albyInfoHub struct {
LatestVersion string `json:"latest_version"`
LatestReleaseNotes string `json:"latest_release_notes"`
}

type albyInfo struct {
Hub albyInfoHub `json:"hub"`
// TODO: consider getting healthcheck/incident info and showing in the hub
}

info := &albyInfo{}
err = json.NewDecoder(res.Body).Decode(info)
if err != nil {
logger.Logger.WithError(err).Error("Failed to decode API response")
return nil, err
}

return &AlbyInfo{
Hub: AlbyInfoHub{
LatestVersion: info.Hub.LatestVersion,
LatestReleaseNotes: info.Hub.LatestReleaseNotes,
},
}, nil
}

func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
token, err := svc.fetchUserToken(ctx)
if err != nil {
Expand All @@ -221,7 +269,7 @@ func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {

client := svc.oauthConf.Client(ctx, token)

req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", svc.cfg.GetEnv().AlbyAPIURL), nil)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/users", albyOAuthAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /me")
return nil, err
Expand Down Expand Up @@ -258,7 +306,7 @@ func (svc *albyOAuthService) GetBalance(ctx context.Context) (*AlbyBalance, erro

client := svc.oauthConf.Client(ctx, token)

req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", svc.cfg.GetEnv().AlbyAPIURL), nil)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/lndhub/balance", albyOAuthAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to balance endpoint")
return nil, err
Expand Down Expand Up @@ -342,7 +390,7 @@ func (svc *albyOAuthService) SendPayment(ctx context.Context, invoice string) er
return err
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", svc.cfg.GetEnv().AlbyAPIURL), body)
req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/lndhub/bolt11", albyOAuthAPIURL), body)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request bolt11 endpoint")
return err
Expand Down Expand Up @@ -615,7 +663,7 @@ func (svc *albyOAuthService) ConsumeEvent(ctx context.Context, event *events.Eve
return
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", svc.cfg.GetEnv().AlbyAPIURL), body)
req, err := http.NewRequest("POST", fmt.Sprintf("%s/events", albyOAuthAPIURL), body)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /events")
return
Expand Down Expand Up @@ -684,7 +732,7 @@ func (svc *albyOAuthService) backupChannels(ctx context.Context, event *events.E
return fmt.Errorf("failed to encode channels backup request payload: %w", err)
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", svc.cfg.GetEnv().AlbyAPIURL), body)
req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/backups", albyOAuthAPIURL), body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
Expand Down Expand Up @@ -727,7 +775,7 @@ func (svc *albyOAuthService) createAlbyAccountNWCNode(ctx context.Context) (stri
return "", err
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", svc.cfg.GetEnv().AlbyAPIURL), body)
req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), body)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /internal/nwcs")
return "", err
Expand Down Expand Up @@ -777,7 +825,7 @@ func (svc *albyOAuthService) destroyAlbyAccountNWCNode(ctx context.Context) erro

client := svc.oauthConf.Client(ctx, token)

req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/internal/nwcs", svc.cfg.GetEnv().AlbyAPIURL), nil)
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/internal/nwcs", albyOAuthAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /internal/nwcs")
return err
Expand Down Expand Up @@ -811,7 +859,7 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err

client := svc.oauthConf.Client(ctx, token)

req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", svc.cfg.GetEnv().AlbyAPIURL), nil)
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/internal/nwcs/activate", albyOAuthAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request /internal/nwcs/activate")
return err
Expand Down Expand Up @@ -839,15 +887,9 @@ func (svc *albyOAuthService) activateAlbyAccountNWCNode(ctx context.Context) err

func (svc *albyOAuthService) GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error) {

token, err := svc.fetchUserToken(ctx)
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch user token")
return nil, err
}

client := svc.oauthConf.Client(ctx, token)
client := &http.Client{Timeout: 10 * time.Second}

req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", svc.cfg.GetEnv().AlbyAPIURL), nil)
req, err := http.NewRequest("GET", fmt.Sprintf("%s/internal/channel_suggestions", albyInternalAPIURL), nil)
if err != nil {
logger.Logger.WithError(err).Error("Error creating request to channel_suggestions endpoint")
return nil, err
Expand Down
14 changes: 12 additions & 2 deletions alby/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

type AlbyOAuthService interface {
events.EventSubscriber
GetInfo(ctx context.Context) (*AlbyInfo, error)
GetChannelPeerSuggestions(ctx context.Context) ([]ChannelPeerSuggestion, error)
GetAuthUrl() string
GetUserIdentifier() (string, error)
Expand Down Expand Up @@ -47,9 +48,18 @@ type AutoChannelResponse struct {
Fee uint64 `json:"fee"`
}

type AlbyInfoHub struct {
LatestVersion string `json:"latestVersion"`
LatestReleaseNotes string `json:"latestReleaseNotes"`
}

type AlbyInfo struct {
Hub AlbyInfoHub `json:"hub"`
// TODO: consider getting healthcheck/incident info and showing in the hub
}

type AlbyMeHub struct {
LatestVersion string `json:"latest_version"`
Name string `json:"name"`
Name string `json:"name"`
}
type AlbyMe struct {
Identifier string `json:"identifier"`
Expand Down
2 changes: 0 additions & 2 deletions config/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,8 @@ type AppConfig struct {
LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"`
LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"`
AlbyAPIURL string `envconfig:"ALBY_API_URL" default:"https://api.getalby.com"`
AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"`
AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"`
AlbyOAuthAuthUrl string `envconfig:"ALBY_OAUTH_AUTH_URL" default:"https://getalby.com/oauth"`
BaseUrl string `envconfig:"BASE_URL"`
FrontendUrl string `envconfig:"FRONTEND_URL"`
LogEvents bool `envconfig:"LOG_EVENTS" default:"true"`
Expand Down
75 changes: 50 additions & 25 deletions frontend/src/components/layouts/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Megaphone,
Menu,
MessageCircleQuestion,
PlugZapIcon,
Settings,
ShieldAlertIcon,
ShieldCheckIcon,
Expand Down Expand Up @@ -46,6 +47,7 @@ import {
} from "src/components/ui/tooltip";
import { useAlbyMe } from "src/hooks/useAlbyMe";

import { useAlbyInfo } from "src/hooks/useAlbyInfo";
import { useInfo } from "src/hooks/useInfo";
import { useRemoveSuccessfulChannelOrder } from "src/hooks/useRemoveSuccessfulChannelOrder";
import { deleteAuthToken } from "src/lib/auth";
Expand Down Expand Up @@ -88,15 +90,28 @@ export default function AppLayout() {
return (
<DropdownMenuContent align="end">
<DropdownMenuGroup>
<DropdownMenuItem>
<ExternalLink
to="https://getalby.com/settings"
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>Alby Account Settings</p>
</ExternalLink>
</DropdownMenuItem>
{!info?.albyAccountConnected && (
<DropdownMenuItem>
<Link
to="/alby/account"
className="w-full flex flex-row items-center gap-2"
>
<PlugZapIcon className="w-4 h-4" />
<p>Connect Alby Account</p>
</Link>
</DropdownMenuItem>
)}
{info?.albyAccountConnected && (
<DropdownMenuItem>
<ExternalLink
to="https://getalby.com/settings"
className="w-full flex flex-row items-center gap-2"
>
<ExternalLinkIcon className="w-4 h-4" />
<p>Alby Account Settings</p>
</ExternalLink>
</DropdownMenuItem>
)}
</DropdownMenuGroup>
<DropdownMenuSeparator />
{isHttpMode && (
Expand Down Expand Up @@ -183,7 +198,7 @@ export default function AppLayout() {
<MessageCircleQuestion className="h-4 w-4" />
Live Support
</MenuItem>
{!albyMe?.hub.name && (
{!albyMe?.hub.name && info?.albyAccountConnected && (
<MenuItem
to="/"
onClick={(e) => {
Expand All @@ -204,7 +219,7 @@ export default function AppLayout() {
<div className="font-sans min-h-screen w-full flex flex-col">
<div className="flex-1 h-full md:grid md:grid-cols-[280px_minmax(0,1fr)]">
<div className="hidden border-r bg-muted/40 md:block">
<div className="flex h-full max-h-screen flex-col gap-2 sticky top-0 overflow-y-auto">
<div className="flex h-full max-h-screen flex-col gap-2 sticky z-10 top-0 overflow-y-auto">
<div className="flex-1">
<nav className="grid items-start px-4 py-2 text-sm font-medium">
<div className="p-3 flex justify-between items-center mt-2 mb-6">
Expand All @@ -220,15 +235,19 @@ export default function AppLayout() {
<SidebarHint />
<MainNavSecondary />
<div className="flex h-14 items-center px-4 gap-3 border-t border-border justify-between">
<div className="grid grid-flow-col gap-2">
<UserAvatar className="h-8 w-8" />
<Link
to="#"
className="font-semibold text-lg whitespace-nowrap overflow-hidden text-ellipsis"
>
{albyMe?.name || albyMe?.email}
</Link>
</div>
{info.albyAccountConnected ? (
<div className="grid grid-flow-col gap-2 items-center">
<UserAvatar className="h-8 w-8" />
<Link
to="#"
className="font-semibold text-lg whitespace-nowrap overflow-hidden text-ellipsis"
>
{albyMe?.name || albyMe?.email}
</Link>
</div>
) : (
<div></div>
)}
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
Expand Down Expand Up @@ -299,17 +318,16 @@ export default function AppLayout() {
}

function AppVersion() {
const { data: albyMe } = useAlbyMe();
const { data: albyInfo } = useAlbyInfo();
const { data: info } = useInfo();
if (!info || !albyMe) {
if (!info || !albyInfo) {
return null;
}

const upToDate =
info.version &&
albyMe.hub.latest_version &&
info.version.startsWith("v") &&
info.version.substring(1) >= albyMe?.hub.latest_version;
info.version.substring(1) >= albyInfo.hub.latestVersion;

return (
<TooltipProvider>
Expand All @@ -333,7 +351,14 @@ function AppVersion() {
{upToDate ? (
<p>Alby Hub is up to date!</p>
) : (
<p>Alby Hub {albyMe?.hub.latest_version} available!</p>
<div>
<p className="font-semibold">
Alby Hub {albyInfo.hub.latestVersion} available!
</p>
<p className="mt-2 max-w-xs whitespace-pre-wrap">
{albyInfo.hub.latestReleaseNotes}
</p>
</div>
)}
</TooltipContent>
</Tooltip>
Expand Down
14 changes: 12 additions & 2 deletions frontend/src/components/layouts/SettingsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import { cn } from "src/lib/utils";
import { request } from "src/utils/request";

export default function SettingsLayout() {
const { mutate: refetchInfo, hasMnemonic, hasNodeBackup } = useInfo();
const {
data: info,
mutate: refetchInfo,
hasMnemonic,
hasNodeBackup,
} = useInfo();
const navigate = useNavigate();
const { toast } = useToast();
const [shuttingDown, setShuttingDown] = useState(false);
Expand Down Expand Up @@ -102,7 +107,12 @@ export default function SettingsLayout() {
{hasNodeBackup && (
<MenuItem to="/settings/node-backup">Migrate Node</MenuItem>
)}
<MenuItem to="/settings/alby-account">Alby Account</MenuItem>
{info?.albyAccountConnected && (
<MenuItem to="/settings/alby-account">Your Alby Account</MenuItem>
)}
{info && !info.albyAccountConnected && (
<MenuItem to="/alby/account">Alby Account</MenuItem>
)}
<MenuItem to="/settings/developer">Developer</MenuItem>
<MenuItem to="/settings/debug-tools">Debug Tools</MenuItem>
</nav>
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/components/redirects/DefaultRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ export function DefaultRedirect() {
const navigate = useNavigate();

React.useEffect(() => {
if (!info || (info.running && info.unlocked && info.albyAccountConnected)) {
if (
!info ||
(info.running &&
info.unlocked &&
(info.albyAccountConnected || !info.albyUserIdentifier))
) {
return;
}
const returnTo = location.pathname + location.search;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/redirects/HomeRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function HomeRedirect() {
let to: string | undefined;
if (info.setupCompleted && info.running) {
if (info.unlocked) {
if (info.albyAccountConnected) {
if (info.albyAccountConnected || !info.albyUserIdentifier) {
const returnTo = window.localStorage.getItem(
localStorageKeys.returnTo
);
Expand Down
Loading

0 comments on commit 63cd230

Please sign in to comment.