Skip to content

Commit

Permalink
Feat: permissions revamp v2 (#273)
Browse files Browse the repository at this point in the history
* feat: revamp permissions component

* chore: changes

* chore: changes

* chore: add view mode for show app screen

* chore: further changes

* feat: new illustration for linking account (#254)

* feat: new illustration for linking account

* fix: update paths

* fix: icon props (#256)

* fix: use date from frontend

* chore: add expiryselect component

* chore: further changes

* chore: further changes

* chore: add date-fns for date picker

* chore: further changes

* chore: further changes

* fix: add central LDK gossip node to help gossip new public channels (#262)

* fix: make dialog responsive (#258)

* chore: further changes

* typo

* chore: spacing issues

* chore: use scopes from capabilities

* chore: change scope type descriptions

* chore: styling fixes

* chore: fix typings

* chore: budget renewal component

* fix: LDK mark channel as inactive and show error if counterparty forwarding info missing (#267)

fix: mark channel as inactive and show error if counterparty forwarding info missing

* chore: remove unnecessary dark classes (#255)

* fix: links to open first channel in sidebar and onboarding checklist (#268)

* feat: improve migrate node UI (#269)

* fix: migrate node copy (#270)

* fix: stop nostr when app is shutdown and use context to stop lnclient (#271)

* fix: permissions revamp WIP

* feat: basic isolated apps UI

* fix: new app connection, edit app connection, deep linking

---------

Co-authored-by: im-adithya <imadithyavardhan@gmail.com>
Co-authored-by: René Aaron <100827540+reneaaron@users.noreply.github.com>
Co-authored-by: Michael Bumann <hello@michaelbumann.com>
  • Loading branch information
4 people authored Jul 14, 2024
1 parent ae0a1a9 commit e297251
Show file tree
Hide file tree
Showing 41 changed files with 1,147 additions and 529 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ If the client creates the secret the client only needs to share the public key o
- `budget_renewal` (optional) reset the budget at the end of the given budget renewal. Can be `never` (default), `daily`, `weekly`, `monthly`, `yearly`
- `request_methods` (optional) url encoded, space separated list of request types that you need permission for: `pay_invoice` (default), `get_balance` (see NIP47). For example: `..&request_methods=pay_invoice%20get_balance`
- `notification_types` (optional) url encoded, space separated list of notification types that you need permission for: For example: `..&notification_types=payment_received%20payment_sent`
- `isolated` (optional) makes an isolated app connection with its own balance and only access to its own transaction list. e.g. `&isolated=true`. If using this option, you should not pass any custom request methods or notification types, nor set a budget or expiry.

Example:

Expand Down
1 change: 1 addition & 0 deletions alby/alby_oauth_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ func (svc *albyOAuthService) LinkAccount(ctx context.Context, lnClient lnclient.
renewal,
nil,
scopes,
false,
)

if err != nil {
Expand Down
30 changes: 21 additions & 9 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,14 @@ func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppRespons
}
}

app, pairingSecretKey, err := api.dbSvc.CreateApp(createAppRequest.Name, createAppRequest.Pubkey, createAppRequest.MaxAmountSat, createAppRequest.BudgetRenewal, expiresAt, createAppRequest.Scopes)
app, pairingSecretKey, err := api.dbSvc.CreateApp(
createAppRequest.Name,
createAppRequest.Pubkey,
createAppRequest.MaxAmountSat,
createAppRequest.BudgetRenewal,
expiresAt,
createAppRequest.Scopes,
createAppRequest.Isolated)

if err != nil {
return nil, err
Expand Down Expand Up @@ -212,6 +219,11 @@ func (api *api) GetApp(dbApp *db.App) *App {
Scopes: requestMethods,
BudgetUsage: budgetUsage,
BudgetRenewal: paySpecificPermission.BudgetRenewal,
Isolated: dbApp.Isolated,
}

if dbApp.Isolated {
response.Balance = queries.GetIsolatedBalance(api.db, dbApp.ID)
}

if lastEventResult.RowsAffected > 0 {
Expand Down Expand Up @@ -244,6 +256,11 @@ func (api *api) ListApps() ([]App, error) {
CreatedAt: dbApp.CreatedAt,
UpdatedAt: dbApp.UpdatedAt,
NostrPubkey: dbApp.NostrPubkey,
Isolated: dbApp.Isolated,
}

if dbApp.Isolated {
apiApp.Balance = queries.GetIsolatedBalance(api.db, dbApp.ID)
}

for _, appPermission := range permissionsMap[dbApp.ID] {
Expand Down Expand Up @@ -317,15 +334,11 @@ func (api *api) Stop() error {
return errors.New("LNClient not started")
}

// TODO: this should stop everything related to the lnclient
// stop the lnclient
// stop the lnclient, nostr relay etc.
// The user will be forced to re-enter their unlock password to restart the node
err := api.svc.StopLNClient()
if err != nil {
logger.Logger.WithError(err).Error("Failed to stop LNClient")
}
api.svc.StopApp()

return err
return nil
}

func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnectionInfo, error) {
Expand Down Expand Up @@ -746,7 +759,6 @@ func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) {
logger.Logger.WithField("expiresAt", expiresAtString).Error("Invalid expiresAt")
return nil, fmt.Errorf("invalid expiresAt: %v", err)
}
expiresAtValue = time.Date(expiresAtValue.Year(), expiresAtValue.Month(), expiresAtValue.Day(), 23, 59, 59, 0, expiresAtValue.Location())
expiresAt = &expiresAtValue
}
return expiresAt, nil
Expand Down
3 changes: 3 additions & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ type App struct {
MaxAmountSat uint64 `json:"maxAmount"`
BudgetUsage uint64 `json:"budgetUsage"`
BudgetRenewal string `json:"budgetRenewal"`
Isolated bool `json:"isolated"`
Balance uint64 `json:"balance"`
}

type ListAppsResponse struct {
Expand All @@ -89,6 +91,7 @@ type CreateAppRequest struct {
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
ReturnTo string `json:"returnTo"`
Isolated bool `json:"isolated"`
}

type StartRequest struct {
Expand Down
2 changes: 1 addition & 1 deletion cmd/http/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,6 @@ func main() {
defer cancel()
e.Shutdown(ctx)
logger.Logger.Info("Echo server exited")
svc.WaitShutdown()
svc.Shutdown()
logger.Logger.Info("Service exited")
}
16 changes: 14 additions & 2 deletions db/db_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package db

import (
"encoding/hex"
"errors"
"fmt"
"slices"
"time"

"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/events"
"github.com/getAlby/hub/logger"
"github.com/nbd-wtf/go-nostr"
Expand All @@ -23,7 +26,16 @@ func NewDBService(db *gorm.DB, eventPublisher events.EventPublisher) *dbService
}
}

func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error) {
func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool) (*App, string, error) {
if isolated && (slices.Contains(scopes, constants.GET_INFO_SCOPE)) {
// cannot return node info because the isolated app is a custodial subaccount
return nil, "", errors.New("Isolated app cannot have get_info scope")
}
if isolated && (slices.Contains(scopes, constants.SIGN_MESSAGE_SCOPE)) {
// cannot sign messages because the isolated app is a custodial subaccount
return nil, "", errors.New("Isolated app cannot have sign_message scope")
}

var pairingPublicKey string
var pairingSecretKey string
if pubkey == "" {
Expand All @@ -39,7 +51,7 @@ func (svc *dbService) CreateApp(name string, pubkey string, maxAmountSat uint64,
}
}

app := App{Name: name, NostrPubkey: pairingPublicKey}
app := App{Name: name, NostrPubkey: pairingPublicKey, Isolated: isolated}

err := svc.db.Transaction(func(tx *gorm.DB) error {
err := tx.Save(&app).Error
Expand Down
2 changes: 1 addition & 1 deletion db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type Transaction struct {
}

type DBService interface {
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string) (*App, string, error)
CreateApp(name string, pubkey string, maxAmountSat uint64, budgetRenewal string, expiresAt *time.Time, scopes []string, isolated bool) (*App, string, error)
}

const (
Expand Down
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
Expand All @@ -41,12 +42,14 @@
"canvas-confetti": "^1.9.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"dayjs": "^1.11.10",
"embla-carousel-react": "^8.0.2",
"gradient-avatar": "^1.0.2",
"lucide-react": "^0.363.0",
"posthog-js": "^1.116.6",
"react": "^18.2.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-lottie": "^1.2.4",
"react-qr-code": "^2.0.12",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/images/illustrations/alby-account-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
81 changes: 62 additions & 19 deletions frontend/src/components/BudgetAmountSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,74 @@
import React from "react";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { cn } from "src/lib/utils";
import { budgetOptions } from "src/types";

function BudgetAmountSelect({
value,
onChange,
}: {
value?: number;
value: number;
onChange: (value: number) => void;
}) {
const [customBudget, setCustomBudget] = React.useState(
value ? !Object.values(budgetOptions).includes(value) : false
);
return (
<div className="grid grid-cols-6 grid-rows-2 md:grid-rows-1 md:grid-cols-6 gap-2 text-xs">
{Object.keys(budgetOptions).map((budget) => {
const amount = budgetOptions[budget];
return (
<div
key={budget}
onClick={() => onChange(amount)}
className={`col-span-2 md:col-span-1 cursor-pointer rounded border-2 ${
value === amount ? "border-primary" : "border-muted"
} text-center py-4`}
>
{budget}
<br />
{amount ? "sats" : "#reckless"}
</div>
);
})}
</div>
<>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 text-xs mb-4">
{Object.keys(budgetOptions).map((budget) => {
return (
<div
key={budget}
onClick={() => {
setCustomBudget(false);
onChange(budgetOptions[budget]);
}}
className={cn(
"cursor-pointer rounded text-nowrap border-2 text-center p-4",
!customBudget && value == budgetOptions[budget]
? "border-primary"
: "border-muted"
)}
>
{`${budget} ${budgetOptions[budget] ? " sats" : ""}`}
</div>
);
})}
<div
onClick={() => {
setCustomBudget(true);
onChange(0);
}}
className={cn(
"cursor-pointer rounded border-2 text-center p-4 dark:text-white",
customBudget ? "border-primary" : "border-muted"
)}
>
Custom...
</div>
</div>
{customBudget && (
<div className="w-full mb-6">
<Label htmlFor="budget" className="block mb-2">
Custom budget amount (sats)
</Label>
<Input
id="budget"
name="budget"
type="number"
required
autoFocus
min={1}
value={value || ""}
onChange={(e) => {
onChange(parseInt(e.target.value));
}}
/>
</div>
)}
</>
);
}

Expand Down
43 changes: 29 additions & 14 deletions frontend/src/components/BudgetRenewalSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { XIcon } from "lucide-react";
import React from "react";
import { Label } from "src/components/ui/label";
import {
Select,
SelectContent,
Expand All @@ -11,27 +13,40 @@ import { BudgetRenewalType, validBudgetRenewals } from "src/types";
interface BudgetRenewalProps {
value: BudgetRenewalType;
onChange: (value: BudgetRenewalType) => void;
disabled?: boolean;
onClose?: () => void;
}

const BudgetRenewalSelect: React.FC<BudgetRenewalProps> = ({
value,
onChange,
disabled,
onClose,
}) => {
return (
<Select value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder={"placeholder"} />
</SelectTrigger>
<SelectContent>
{validBudgetRenewals.map((renewalOption) => (
<SelectItem key={renewalOption} value={renewalOption}>
{renewalOption.charAt(0).toUpperCase() + renewalOption.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
<>
<Label htmlFor="budget-renewal" className="block mb-2">
Budget Renewal
</Label>
<div className="flex gap-2 items-center text-muted-foreground mb-4 text-sm">
<Select value={value} onValueChange={onChange}>
<SelectTrigger id="budget-renewal" className="w-[150px] capitalize">
<SelectValue placeholder={value} />
</SelectTrigger>
<SelectContent className="capitalize">
{validBudgetRenewals.map((renewalOption) => (
<SelectItem key={renewalOption} value={renewalOption}>
{renewalOption}
</SelectItem>
))}
</SelectContent>
{onClose && (
<XIcon
className="cursor-pointer w-4 text-muted-foreground"
onClick={onClose}
/>
)}
</Select>
</div>
</>
);
};

Expand Down
Loading

0 comments on commit e297251

Please sign in to comment.