Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: NWA auth for self-hosted hubs #1016

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
24 changes: 18 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
"sub-wallets are currently not supported on your node backend. Try LDK or LND")
}

if slices.Contains(createAppRequest.Scopes, constants.SUPERUSER_SCOPE) {
if !api.cfg.CheckUnlockPassword(createAppRequest.UnlockPassword) {
return nil, fmt.Errorf(
"cannot create app with superuser permission without specifying unlock password")
}
}

expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt)
if err != nil {
return nil, fmt.Errorf("invalid expiresAt: %v", err)
Expand Down Expand Up @@ -207,12 +214,17 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
existingScopeMap[perm.Scope] = true
}

if slices.Contains(newScopes, constants.SUPERUSER_SCOPE) && !existingScopeMap[constants.SUPERUSER_SCOPE] {
return fmt.Errorf(
"cannot update app to add superuser permission")
}

// Add new permissions
for _, method := range newScopes {
if !existingScopeMap[method] {
for _, scope := range newScopes {
if !existingScopeMap[scope] {
perm := db.AppPermission{
App: *userApp,
Scope: method,
Scope: scope,
ExpiresAt: expiresAt,
MaxAmountSat: int(maxAmount),
BudgetRenewal: budgetRenewal,
Expand All @@ -221,12 +233,12 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
return err
}
}
delete(existingScopeMap, method)
delete(existingScopeMap, scope)
}

// Remove old permissions
for method := range existingScopeMap {
if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, method).Delete(&db.AppPermission{}).Error; err != nil {
for scope := range existingScopeMap {
if err := tx.Where("app_id = ? AND scope = ?", userApp.ID, scope).Delete(&db.AppPermission{}).Error; err != nil {
return err
}
}
Expand Down
19 changes: 10 additions & 9 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,16 @@ type TopupIsolatedAppRequest struct {
}

type CreateAppRequest struct {
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
ReturnTo string `json:"returnTo"`
Isolated bool `json:"isolated"`
Metadata Metadata `json:"metadata,omitempty"`
Name string `json:"name"`
Pubkey string `json:"pubkey"`
MaxAmountSat uint64 `json:"maxAmount"`
BudgetRenewal string `json:"budgetRenewal"`
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
ReturnTo string `json:"returnTo"`
Isolated bool `json:"isolated"`
Metadata Metadata `json:"metadata,omitempty"`
UnlockPassword string `json:"unlockPassword"`
}

type StartRequest struct {
Expand Down
9 changes: 9 additions & 0 deletions apps/apps_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"slices"
"strings"
"time"

"github.com/getAlby/hub/constants"
Expand Down Expand Up @@ -44,6 +45,14 @@ func (svc *appsService) CreateApp(name string, pubkey string, maxAmountSat uint6
return nil, "", errors.New("Sub-wallet app connection cannot have sign_message scope")
}

if budgetRenewal == "" {
budgetRenewal = constants.BUDGET_RENEWAL_NEVER
}

if !slices.Contains(constants.GetBudgetRenewals(), budgetRenewal) {
return nil, "", fmt.Errorf("invalid budget renewal. Must be one of %s", strings.Join(constants.GetBudgetRenewals(), ","))
}

var pairingPublicKey string
var pairingSecretKey string
if pubkey == "" {
Expand Down
11 changes: 11 additions & 0 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ const (
BUDGET_RENEWAL_NEVER = "never"
)

func GetBudgetRenewals() []string {
return []string{
BUDGET_RENEWAL_DAILY,
BUDGET_RENEWAL_WEEKLY,
BUDGET_RENEWAL_MONTHLY,
BUDGET_RENEWAL_YEARLY,
BUDGET_RENEWAL_NEVER,
}
}

const (
PAY_INVOICE_SCOPE = "pay_invoice" // also covers pay_keysend and multi_* payment methods
GET_BALANCE_SCOPE = "get_balance"
Expand All @@ -28,6 +38,7 @@ const (
LIST_TRANSACTIONS_SCOPE = "list_transactions"
SIGN_MESSAGE_SCOPE = "sign_message"
NOTIFICATIONS_SCOPE = "notifications" // covers all notification types
SUPERUSER_SCOPE = "superuser"
)

// limit encoded metadata length, otherwise relays may have trouble listing multiple transactions
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrickWall, PlusCircle } from "lucide-react";
import { AlertTriangleIcon, BrickWall, PlusCircle } from "lucide-react";
import React from "react";
import BudgetAmountSelect from "src/components/BudgetAmountSelect";
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
Expand Down Expand Up @@ -132,6 +132,20 @@ const Permissions: React.FC<PermissionsProps> = ({
</>
)}

{permissions.scopes.includes("superuser") && (
<>
<div className="flex items-center gap-2 mb-2">
<AlertTriangleIcon className="w-4 h-4" />
<p className="text-sm font-medium">Superuser Access</p>
</div>

<p className="mb-4">
This app can create other app connections. Please make sure you
trust this app.
</p>
</>
)}

{!permissions.isolated && permissions.scopes.includes("pay_invoice") && (
<>
{!readOnly && !budgetReadOnly ? (
Expand Down
13 changes: 10 additions & 3 deletions frontend/src/components/Scopes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const Scopes: React.FC<ScopesProps> = ({
onScopesChanged,
}) => {
const fullAccessScopes: Scope[] = React.useMemo(() => {
return [...capabilities.scopes];
return capabilities.scopes.filter((scope) => scope !== "superuser");
}, [capabilities.scopes]);

const readOnlyScopes: Scope[] = React.useMemo(() => {
Expand Down Expand Up @@ -87,10 +87,17 @@ const Scopes: React.FC<ScopesProps> = ({
}, [capabilities.scopes]);

const [scopeGroup, setScopeGroup] = React.useState<ScopeGroup>(() => {
if (isolated && scopes.length === capabilities.scopes.length) {
if (
isolated &&
scopes.length === fullAccessScopes.length &&
scopes.every((scope) => fullAccessScopes.includes(scope))
) {
return "isolated";
}
if (scopes.length === capabilities.scopes.length) {
if (
scopes.length === fullAccessScopes.length &&
scopes.every((scope) => fullAccessScopes.includes(scope))
) {
return "full_access";
}
if (
Expand Down
66 changes: 13 additions & 53 deletions frontend/src/components/SuggestedAppData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ export const suggestedApps: SuggestedApp[] = [
internal: true,
logo: uncleJim,
},
{
id: "alby-go",
title: "Alby Go",
description: "A simple mobile wallet that works great with Alby Hub",
webLink: "https://albygo.com",
playLink:
"https://play.google.com/store/apps/details?id=com.getalby.mobile",
appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774",
zapStoreLink: "https://zapstore.dev/download/",
logo: albyGo,
extendedDescription: "Sends and receives payments seamlessly from your Hub",
internal: true,
},
{
id: "buzzpay",
title: "BuzzPay PoS",
Expand Down Expand Up @@ -1623,59 +1636,6 @@ export const suggestedApps: SuggestedApp[] = [
</>
),
},
{
id: "alby-go",
title: "Alby Go",
description: "A simple mobile wallet that works great with Alby Hub",
webLink: "https://albygo.com",
playLink:
"https://play.google.com/store/apps/details?id=com.getalby.mobile",
appleLink: "https://apps.apple.com/us/app/alby-go/id6471335774",
zapStoreLink: "https://zapstore.dev/download/",
logo: albyGo,
extendedDescription: "Sends and receives payments seamlessly from your Hub",
guide: (
<>
<div>
<h3 className="font-medium">In Alby Go</h3>
<ul className="list-inside text-muted-foreground">
<li>
1. Download and open{" "}
<span className="font-medium text-foreground">Alby Go</span> on
your Android or iOS device
</li>
<li>
2. Click on{" "}
<span className="font-medium text-foreground">
Connect Wallet
</span>
</li>
</ul>
</div>
<div>
<h3 className="font-medium">In Alby Hub</h3>
<ul className="list-inside text-muted-foreground">
<li>
4. Click{" "}
<Link
to="/apps/new?app=alby-go"
className="font-medium text-foreground underline"
>
Connect to Alby Go
</Link>
</li>
<li>5. Set app's wallet permissions (full access recommended)</li>
</ul>
</div>
<div>
<h3 className="font-medium">In Alby Go</h3>
<ul className="list-inside text-muted-foreground">
<li>6. Scan or paste the connection secret from Alby Hub</li>
</ul>
</div>
</>
),
},
{
id: "pullthatupjamie-ai",
title: "Pull That Up Jamie!",
Expand Down
31 changes: 29 additions & 2 deletions frontend/src/components/connections/AppCardConnectionInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dayjs from "dayjs";
import { BrickWall, CircleCheck, PlusCircle } from "lucide-react";
import { BrickWall, CircleCheck, Crown, PlusCircle } from "lucide-react";
import { Link } from "react-router-dom";
import { Button } from "src/components/ui/button";
import { Progress } from "src/components/ui/progress";
Expand Down Expand Up @@ -34,7 +34,34 @@ export function AppCardConnectionInfo({

return (
<>
{connection.isolated ? (
{connection.scopes.indexOf("superuser") > -1 ? (
<>
<div className="text-sm text-secondary-foreground font-medium w-full h-full flex flex-col gap-2">
<div className="flex flex-row items-center gap-2">
<Crown className="w-4 h-4" />
Superuser
</div>
</div>
<div className="flex flex-row justify-between mt-2">
<div className="mb-2">
<p className="text-xs text-secondary-foreground font-medium">
You've spent
</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(connection.budgetUsage)} sats
</p>
</div>
</div>
<div className="flex flex-row justify-between text-xs items-end mt-2">
<div className="text-muted-foreground">
Last used:{" "}
{connection.lastEventAt
? dayjs(connection.lastEventAt).fromNow()
: "Never"}
</div>
</div>
</>
) : connection.isolated ? (
<>
<div className="text-sm text-secondary-foreground font-medium w-full h-full flex flex-col gap-2">
<div className="flex flex-row items-center gap-2">
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { OpeningAutoChannel } from "src/screens/channels/auto/OpeningAutoChannel
import { FirstChannel } from "src/screens/channels/first/FirstChannel";
import { OpenedFirstChannel } from "src/screens/channels/first/OpenedFirstChannel";
import { OpeningFirstChannel } from "src/screens/channels/first/OpeningFirstChannel";
import { AlbyGo } from "src/screens/internal-apps/AlbyGo";
import { BuzzPay } from "src/screens/internal-apps/BuzzPay";
import { SimpleBoost } from "src/screens/internal-apps/SimpleBoost";
import { UncleJim } from "src/screens/internal-apps/UncleJim";
Expand Down Expand Up @@ -240,6 +241,10 @@ const routes = [
path: "uncle-jim",
element: <UncleJim />,
},
{
path: "alby-go",
element: <AlbyGo />,
},
{
path: "buzzpay",
element: <BuzzPay />,
Expand Down
Loading
Loading