DeveloperSettings.tsx raw

   1  import { CopyIcon, SquareArrowOutUpRightIcon } from "lucide-react";
   2  import React from "react";
   3  import { toast } from "sonner";
   4  import PasswordInput from "src/components/password/PasswordInput";
   5  import SettingsHeader from "src/components/SettingsHeader";
   6  import { Button } from "src/components/ui/button";
   7  import { ExternalLinkButton } from "src/components/ui/custom/external-link-button";
   8  import { LoadingButton } from "src/components/ui/custom/loading-button";
   9  import { Input } from "src/components/ui/input";
  10  import { Label } from "src/components/ui/label";
  11  import { RadioGroup, RadioGroupItem } from "src/components/ui/radio-group";
  12  import { Separator } from "src/components/ui/separator";
  13  import { useAlbyMe } from "src/hooks/useAlbyMe";
  14  import { copyToClipboard } from "src/lib/clipboard";
  15  import { AuthTokenResponse } from "src/types";
  16  import { isHttpMode } from "src/utils/isHttpMode";
  17  import { request } from "src/utils/request";
  18  
  19  export default function DeveloperSettings() {
  20    const { data: albyMe } = useAlbyMe();
  21    const _isHttpMode = isHttpMode();
  22    const [token, setToken] = React.useState<string>();
  23    const [tokenPermission, setTokenPermission] = React.useState<string>();
  24    const [expiryDays, setExpiryDays] = React.useState<string>("365");
  25    const [unlockPassword, setUnlockPassword] = React.useState<string>("");
  26    const [permission, setPermission] = React.useState<"full" | "readonly">(
  27      "full"
  28    );
  29    const [showCreateTokenForm, setShowCreateTokenForm] =
  30      React.useState<boolean>();
  31    const [loading, setLoading] = React.useState<boolean>();
  32  
  33    async function createToken(e: React.FormEvent) {
  34      e.preventDefault();
  35      try {
  36        setLoading(true);
  37        if (!expiryDays || !unlockPassword || !permission) {
  38          throw new Error("Form not filled");
  39        }
  40        const authTokenResponse = await request<AuthTokenResponse>(
  41          "/api/unlock",
  42          {
  43            method: "POST",
  44            headers: {
  45              "Content-Type": "application/json",
  46            },
  47            body: JSON.stringify({
  48              unlockPassword,
  49              tokenExpiryDays: +expiryDays,
  50              permission,
  51            }),
  52          }
  53        );
  54        if (authTokenResponse) {
  55          setToken(authTokenResponse.token);
  56          setTokenPermission(permission);
  57        }
  58      } catch (error) {
  59        console.error(error);
  60        toast.error("Something went wrong", {
  61          description: "" + error,
  62        });
  63      }
  64      setLoading(false);
  65    }
  66  
  67    return (
  68      <>
  69        <SettingsHeader
  70          pageTitle="Developer"
  71          title="Developer"
  72          description="Power your apps with Alby Hub."
  73        />
  74        <div className="flex flex-col gap-4">
  75          <div className="flex flex-col gap-1 text-sm">
  76            <h3 className="font-semibold">Power your apps with NWC</h3>
  77  
  78            <div className="text-muted-foreground">
  79              Alby Hub can power your lightning apps and services in all
  80              environments using Nostr Wallet Connect, an open protocol to connect
  81              lightning wallets and apps
  82            </div>
  83          </div>
  84          <div>
  85            <ExternalLinkButton
  86              size={"lg"}
  87              variant={"secondary"}
  88              to="https://nwc.dev"
  89              className="flex-1 gap-2 items-center justify-center"
  90            >
  91              Learn More on nwc.dev
  92              <SquareArrowOutUpRightIcon />
  93            </ExternalLinkButton>
  94          </div>
  95        </div>
  96        <Separator />
  97        <div className="flex flex-col gap-4">
  98          <div className="flex flex-col gap-1 text-sm">
  99            <h3 className="font-semibold">Experimental API access</h3>
 100  
 101            <div className="text-muted-foreground">
 102              You can use your auth token to access the Alby Hub internal API.
 103              However, whenever possible, we recommend using the NWC API directly
 104              for more stability. Please note that the internal API may change or
 105              be removed entirely in the future.
 106            </div>
 107          </div>
 108          {!_isHttpMode && (
 109            <div className="text-sm text-muted-foreground">
 110              API access is not available in the desktop app because it does not
 111              expose an HTTP API. To use the API, install the Alby Hub server
 112              version instead.
 113            </div>
 114          )}
 115          {_isHttpMode && !token && !showCreateTokenForm && (
 116            <div>
 117              <Button
 118                size={"lg"}
 119                variant={"secondary"}
 120                onClick={() => setShowCreateTokenForm(true)}
 121                className="flex-1"
 122              >
 123                Configure Token
 124              </Button>
 125            </div>
 126          )}
 127          {showCreateTokenForm && !token && (
 128            <form
 129              onSubmit={createToken}
 130              className="w-full md:w-96 flex flex-col gap-4"
 131            >
 132              <>
 133                <div className="grid gap-3">
 134                  <Label>Token Type</Label>
 135                  <RadioGroup
 136                    value={permission}
 137                    onValueChange={(v) => {
 138                      if (v != "readonly" && v !== "full") {
 139                        throw new Error("Unknown permission type");
 140                      }
 141                      setPermission(v);
 142                    }}
 143                    className="mt-4 gap-4"
 144                  >
 145                    <div className="flex items-start space-x-2">
 146                      <RadioGroupItem value="full" id="full" />
 147                      <Label
 148                        htmlFor="full"
 149                        className="flex-1 flex flex-col justify-center items-start cursor-pointer"
 150                      >
 151                        <div className="font-medium shrink-0">Full Access</div>
 152                        <div className="text-sm text-muted-foreground">
 153                          Complete control over your hub - can read data and
 154                          perform all operations (send payments, manage apps,
 155                          etc.)
 156                        </div>
 157                      </Label>
 158                    </div>
 159                    <div className="flex items-start space-x-2">
 160                      <RadioGroupItem value="readonly" id="readonly" />
 161                      <Label
 162                        htmlFor="readonly"
 163                        className="flex-1 flex flex-col justify-center items-start cursor-pointer"
 164                      >
 165                        <div className="font-medium">Read-Only Access</div>
 166                        <div className="text-sm text-muted-foreground">
 167                          View-only access - can read balances, transactions, and
 168                          other data but cannot perform operations
 169                        </div>
 170                      </Label>
 171                    </div>
 172                  </RadioGroup>
 173                </div>
 174                <div className="grid gap-2">
 175                  <Label htmlFor="token-expiry">Token Expiry (Days)</Label>
 176                  <Input
 177                    type="number"
 178                    name="token-expiry"
 179                    id="token-expiry"
 180                    onChange={(e) => setExpiryDays(e.target.value)}
 181                    value={expiryDays}
 182                  />
 183                </div>
 184                <div className="grid gap-2">
 185                  <Label htmlFor="password">Unlock Password</Label>
 186                  <PasswordInput
 187                    id="password"
 188                    onChange={setUnlockPassword}
 189                    value={unlockPassword}
 190                    autoFocus
 191                  />
 192                </div>
 193                <div className="mt-4">
 194                  <LoadingButton loading={loading}>Create Token</LoadingButton>
 195                </div>
 196              </>
 197            </form>
 198          )}
 199          {token && (
 200            <>
 201              <div className="flex flex-row items-center gap-2 mb-2">
 202                <PasswordInput readOnly value={token} className="flex-1" />
 203                <Button
 204                  type="button"
 205                  variant="secondary"
 206                  size="icon"
 207                  onClick={() => {
 208                    copyToClipboard(token);
 209                  }}
 210                >
 211                  <CopyIcon className="size-4" />
 212                </Button>
 213              </div>
 214              <div className="my-4 border rounded-lg p-4">
 215                <p className="mb-2">
 216                  To make requests from your application to{" "}
 217                  <span className="font-mono font-semibold">
 218                    {window.location.origin}/api
 219                  </span>{" "}
 220                  add the following header{albyMe?.hub.name && "s"}:
 221                </p>
 222                <ol>
 223                  <li>
 224                    <span className="font-mono font-semibold">
 225                      Authorization: Bearer YOUR_TOKEN
 226                    </span>{" "}
 227                  </li>
 228                  {albyMe?.hub.name && (
 229                    <li>
 230                      <span className="font-mono font-semibold">
 231                        AlbyHub-Name: {albyMe.hub.name}
 232                      </span>{" "}
 233                    </li>
 234                  )}
 235                  {albyMe?.hub.name && (
 236                    <li>
 237                      <span className="font-mono font-semibold">
 238                        AlbyHub-Region: {albyMe.hub.config?.region || "lax"}
 239                      </span>
 240                    </li>
 241                  )}
 242                </ol>
 243              </div>
 244  
 245              <p className="text-xs">
 246                {tokenPermission === "readonly" ? (
 247                  <>
 248                    This is a read-only token that can view data but cannot
 249                    perform operations like sending payments. Please keep it
 250                    secure.
 251                  </>
 252                ) : (
 253                  <>
 254                    This token grants full access to your hub. Please keep it
 255                    secure.
 256                  </>
 257                )}{" "}
 258                If you suspect that the token has been compromised, immediately
 259                change your unlock password.
 260              </p>
 261            </>
 262          )}
 263        </div>
 264      </>
 265    );
 266  }
 267