Skip to content

Commit

Permalink
feat: improve isolated app support (#786)
Browse files Browse the repository at this point in the history
* feat: add isolated checkbox to create new custom app

* feat: improved isolated app editing

- toggle app between isolated and non isolated
- confirmation for changing isolated app to not isolated
- edit scopes for isolated apps
- move isolated app support check to backend

* chore: improve confirm app dialog copy
  • Loading branch information
rolznz authored Nov 27, 2024
1 parent 2f83fdf commit aa1eeec
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 78 deletions.
19 changes: 18 additions & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ func NewAPI(svc service.Service, gormDB *gorm.DB, config config.Config, keys key
}

func (api *api) CreateApp(createAppRequest *CreateAppRequest) (*CreateAppResponse, error) {
backendType, _ := api.cfg.Get("LNBackendType", "")
if createAppRequest.Isolated &&
backendType != "LDK" &&
backendType != "LND" {
return nil, fmt.Errorf(
"isolated apps are currently not supported on your node backend. Try LDK or LND")
}

expiresAt, err := api.parseExpiresAt(createAppRequest.ExpiresAt)
if err != nil {
return nil, fmt.Errorf("invalid expiresAt: %v", err)
Expand Down Expand Up @@ -152,6 +160,15 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
}
}

// Update app isolation if it is not the same
if updateAppRequest.Isolated != userApp.Isolated {
err := tx.Model(&db.App{}).Where("id", userApp.ID).Update("isolated", updateAppRequest.Isolated).Error
if err != nil {
return err
}
}

// Update the app metadata
if updateAppRequest.Metadata != nil {
var metadataBytes []byte
var err error
Expand All @@ -167,7 +184,7 @@ func (api *api) UpdateApp(userApp *db.App, updateAppRequest *UpdateAppRequest) e
}

// Update existing permissions with new budget and expiry
err := tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{
err = tx.Model(&db.AppPermission{}).Where("app_id", userApp.ID).Updates(map[string]interface{}{
"ExpiresAt": expiresAt,
"MaxAmountSat": maxAmount,
"BudgetRenewal": budgetRenewal,
Expand Down
1 change: 1 addition & 0 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type UpdateAppRequest struct {
ExpiresAt string `json:"expiresAt"`
Scopes []string `json:"scopes"`
Metadata Metadata `json:"metadata,omitempty"`
Isolated bool `json:"isolated"`
}

type TopupIsolatedAppRequest struct {
Expand Down
92 changes: 49 additions & 43 deletions frontend/src/components/Permissions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PlusCircle } from "lucide-react";
import { 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 @@ -85,14 +85,6 @@ const Permissions: React.FC<PermissionsProps> = ({

return (
<div className="max-w-lg">
{permissions.isolated && (
<p className="mb-4">
This app is isolated from the rest of your wallet. This means it will
have an isolated balance and only has access to its own transaction
history. It will not be able to sign messages on your node's behalf.
</p>
)}

{!readOnly && !scopesReadOnly ? (
<Scopes
capabilities={capabilities}
Expand All @@ -103,7 +95,7 @@ const Permissions: React.FC<PermissionsProps> = ({
/>
) : (
<>
<p className="text-sm font-medium mb-2">Scopes</p>
<p className="text-sm font-medium mb-2">This app can:</p>
<div className="flex flex-col mb-2">
{[...permissions.scopes].map((scope) => {
const PermissionIcon = scopeIconMap[scope];
Expand All @@ -124,6 +116,22 @@ const Permissions: React.FC<PermissionsProps> = ({
</>
)}

{permissions.isolated && (
<>
<div className="flex items-center gap-2 mb-2">
<BrickWall className="w-4 h-4" />
<p className="text-sm font-medium">Isolated App</p>
</div>

<p className="mb-4">
This app is isolated from the rest of your wallet. This means it
will have an isolated balance and only has access to its own
transaction history. It will not be able to sign messages on your
node's behalf.
</p>
</>
)}

{!permissions.isolated && permissions.scopes.includes("pay_invoice") && (
<>
{!readOnly && !budgetReadOnly ? (
Expand Down Expand Up @@ -187,40 +195,38 @@ const Permissions: React.FC<PermissionsProps> = ({
</>
)}

{!permissions.isolated && (
<>
{!readOnly && !expiresAtReadOnly ? (
<>
{!showExpiryOptions && (
<Button
type="button"
variant="secondary"
onClick={() => setShowExpiryOptions(true)}
>
<PlusCircle className="w-4 h-4 mr-2" />
Set expiration time
</Button>
)}
<>
{!readOnly && !expiresAtReadOnly ? (
<>
{!showExpiryOptions && (
<Button
type="button"
variant="secondary"
onClick={() => setShowExpiryOptions(true)}
>
<PlusCircle className="w-4 h-4 mr-2" />
Set expiration time
</Button>
)}

{showExpiryOptions && (
<ExpirySelect
value={permissions.expiresAt}
onChange={handleExpiryChange}
/>
)}
</>
) : (
<>
<p className="text-sm font-medium mb-2">Connection expiry</p>
<p className="text-muted-foreground text-sm">
{permissions.expiresAt
? new Date(permissions.expiresAt).toString()
: "This app will never expire"}
</p>
</>
)}
</>
)}
{showExpiryOptions && (
<ExpirySelect
value={permissions.expiresAt}
onChange={handleExpiryChange}
/>
)}
</>
) : (
<>
<p className="text-sm font-medium mb-2">Connection expiry</p>
<p className="text-muted-foreground text-sm">
{permissions.expiresAt
? new Date(permissions.expiresAt).toString()
: "This app will never expire"}
</p>
</>
)}
</>
</div>
);
};
Expand Down
46 changes: 16 additions & 30 deletions frontend/src/components/Scopes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
import React from "react";
import { Checkbox } from "src/components/ui/checkbox";
import { Label } from "src/components/ui/label";
import { useToast } from "src/components/ui/use-toast";
import { useInfo } from "src/hooks/useInfo";
import { cn } from "src/lib/utils";
import { Scope, WalletCapabilities, scopeDescriptions } from "src/types";

Expand Down Expand Up @@ -51,11 +49,8 @@ const Scopes: React.FC<ScopesProps> = ({
capabilities,
scopes,
isolated,
isNewConnection,
onScopesChanged,
}) => {
const { data: info } = useInfo();
const { toast } = useToast();
const fullAccessScopes: Scope[] = React.useMemo(() => {
return [...capabilities.scopes];
}, [capabilities.scopes]);
Expand Down Expand Up @@ -92,7 +87,7 @@ const Scopes: React.FC<ScopesProps> = ({
}, [capabilities.scopes]);

const [scopeGroup, setScopeGroup] = React.useState<ScopeGroup>(() => {
if (isolated) {
if (isolated && scopes.length === capabilities.scopes.length) {
return "isolated";
}
if (scopes.length === capabilities.scopes.length) {
Expand Down Expand Up @@ -135,7 +130,7 @@ const Scopes: React.FC<ScopesProps> = ({
newScopes.push(scope);
}

onScopesChanged(newScopes, false);
onScopesChanged(newScopes, isolated);
};

return (
Expand All @@ -150,28 +145,7 @@ const Scopes: React.FC<ScopesProps> = ({
key={index}
className={`flex flex-col items-center border-2 rounded cursor-pointer ${scopeGroup == sg ? "border-primary" : "border-muted"} p-4`}
onClick={() => {
try {
if (
sg === "isolated" &&
info?.backendType !== "LDK" &&
info?.backendType !== "LND"
) {
throw new Error(
"Isolated apps are currently not supported on your node backend. Try LDK to access all Alby Hub features."
);
}
if (!isNewConnection && !isolated && sg === "isolated") {
// do not allow user to change non-isolated connection to isolated
throw new Error(
"Please create a new isolated connection instead"
);
}
handleScopeGroupChange(sg);
} catch (error) {
toast({
title: "" + error,
});
}
handleScopeGroupChange(sg);
}}
>
<ScopeGroupIcon className="mb-2" />
Expand All @@ -187,7 +161,19 @@ const Scopes: React.FC<ScopesProps> = ({

{scopeGroup == "custom" && (
<div className="mb-2">
<p className="font-medium text-sm">Authorize the app to:</p>
<p className="font-medium text-sm mt-4">Isolation</p>
<div className="flex items-center mt-2">
<Checkbox
id="isolated"
className="mr-2"
onCheckedChange={() => onScopesChanged(scopes, !isolated)}
checked={isolated}
/>
<Label htmlFor="isolated" className="cursor-pointer">
Isolate this app's balance and transactions
</Label>
</div>
<p className="font-medium text-sm mt-4">Authorize the app to:</p>
<ul className="flex flex-col w-full mt-2">
{capabilities.scopes.map((scope, index) => {
return (
Expand Down
45 changes: 41 additions & 4 deletions frontend/src/screens/apps/ShowApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
budgetRenewal: permissions.budgetRenewal,
expiresAt: permissions.expiresAt?.toISOString(),
maxAmount: permissions.maxAmount,
isolated: permissions.isolated,
};

await request(`/api/apps/${app.appPubkey}`, {
Expand Down Expand Up @@ -293,13 +294,49 @@ function AppInternal({ app, refetchApp, capabilities }: AppInternalProps) {
Cancel
</Button>

<Button type="button" onClick={handleSave}>
Save
</Button>
{(app.isolated && !permissions.isolated) ||
(!app.scopes.includes("pay_invoice") &&
permissions.scopes.includes("pay_invoice")) ? (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button type="button">Save</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>
Confirm Update App
</AlertDialogTitle>
<AlertDialogDescription>
{app.isolated && !permissions.isolated ? (
<b>
Are you sure you wish to remove the isolated
status from this connection?
</b>
) : (
<b>
Are you sure you wish to give this
connection pay permissions?
</b>
)}
</AlertDialogDescription>
<AlertDialogFooter className="mt-5">
<AlertDialogCancel
onClick={() => {
window.location.reload();
}}
>
Cancel
</AlertDialogCancel>
<Button onClick={handleSave}>Save</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : (
<Button onClick={handleSave}>Save</Button>
)}
</div>
)}

{!app.isolated && !isEditingPermissions && (
{!isEditingPermissions && (
<>
<Button
variant="outline"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export type UpdateAppRequest = {
expiresAt: string | undefined;
scopes: Scope[];
metadata?: AppMetadata;
isolated: boolean;
};

export type Channel = {
Expand Down

0 comments on commit aa1eeec

Please sign in to comment.