Unlock.tsx raw

   1  import React from "react";
   2  import { useLocation, useNavigate } from "react-router";
   3  import PasswordInput from "src/components/password/PasswordInput";
   4  import TwoColumnLayoutHeader from "src/components/TwoColumnLayoutHeader";
   5  import { LoadingButton } from "src/components/ui/custom/loading-button";
   6  import { Label } from "src/components/ui/label";
   7  
   8  import { useInfo } from "src/hooks/useInfo";
   9  import { saveAuthToken } from "src/lib/auth";
  10  import { hasNip07Signer } from "src/lib/nip07";
  11  import { AuthTokenResponse } from "src/types";
  12  import { handleRequestError } from "src/utils/handleRequestError";
  13  import { request } from "src/utils/request";
  14  
  15  export default function Unlock() {
  16    const [unlockPassword, setUnlockPassword] = React.useState("");
  17    const [loading, setLoading] = React.useState(false);
  18    const navigate = useNavigate();
  19    const location = useLocation();
  20  
  21    const { data: info } = useInfo();
  22    const { mutate: refetchInfo } = useInfo();
  23  
  24    React.useEffect(() => {
  25      if (!info || info.running) {
  26        return;
  27      }
  28      navigate("/");
  29    }, [info, location, navigate]);
  30  
  31    async function onSubmit(e: React.FormEvent) {
  32      e.preventDefault();
  33      try {
  34        setLoading(true);
  35  
  36        const authTokenResponse = await request<AuthTokenResponse>(
  37          "/api/unlock",
  38          {
  39            method: "POST",
  40            headers: {
  41              "Content-Type": "application/json",
  42            },
  43            body: JSON.stringify({
  44              unlockPassword,
  45              permission: "full",
  46            }),
  47          }
  48        );
  49        if (authTokenResponse) {
  50          saveAuthToken(authTokenResponse.token);
  51        }
  52        await refetchInfo();
  53        navigate("/");
  54      } catch (error) {
  55        handleRequestError("Failed to connect", error);
  56      } finally {
  57        setLoading(false);
  58      }
  59    }
  60  
  61    async function onNip07SignIn() {
  62      try {
  63        setLoading(true);
  64  
  65        const challengeResponse = await request<{
  66          challenge: string;
  67          pubkey: string;
  68        }>("/api/nip07/challenge");
  69        if (!challengeResponse) {
  70          throw new Error("Failed to get challenge");
  71        }
  72  
  73        const signedEvent = await window.nostr!.signEvent({
  74          kind: 27235,
  75          created_at: Math.floor(Date.now() / 1000),
  76          tags: [["challenge", challengeResponse.challenge]],
  77          content: "",
  78        });
  79  
  80        await request("/api/nip07/auth", {
  81          method: "POST",
  82          headers: {
  83            "Content-Type": "application/json",
  84          },
  85          body: JSON.stringify({
  86            challenge: challengeResponse.challenge,
  87            event: signedEvent,
  88          }),
  89        });
  90  
  91        await refetchInfo();
  92        navigate("/");
  93      } catch (error) {
  94        handleRequestError("Failed to sign in", error);
  95      } finally {
  96        setLoading(false);
  97      }
  98    }
  99  
 100    if (info?.nip07AuthEnabled) {
 101      return (
 102        <div className="w-full p-5">
 103          <div className="mx-auto grid w-80 max-w-full gap-6">
 104            <TwoColumnLayoutHeader
 105              title="Login"
 106              pageTitle="Login"
 107              description="Sign in with your Nostr extension to continue"
 108            />
 109            {hasNip07Signer() ? (
 110              <LoadingButton loading={loading} onClick={onNip07SignIn}>
 111                Sign in with Nostr
 112              </LoadingButton>
 113            ) : (
 114              <p className="text-sm text-muted-foreground">
 115                No NIP-07 signer detected. Install a Nostr browser extension (e.g.
 116                Alby) and refresh to sign in.
 117              </p>
 118            )}
 119          </div>
 120        </div>
 121      );
 122    }
 123  
 124    return (
 125      <form onSubmit={onSubmit} className="w-full p-5">
 126        <div className="mx-auto grid w-80 max-w-full gap-6">
 127          <TwoColumnLayoutHeader
 128            title="Login"
 129            pageTitle="Login"
 130            description="Enter your unlock password to continue"
 131          />
 132          <div className="grid gap-4">
 133            <div className="grid gap-1.5">
 134              <Label htmlFor="password">Password</Label>
 135              <PasswordInput
 136                id="password"
 137                onChange={setUnlockPassword}
 138                autoFocus
 139                value={unlockPassword}
 140              />
 141            </div>
 142            <LoadingButton type="submit" loading={loading}>
 143              Login
 144            </LoadingButton>
 145          </div>
 146        </div>
 147      </form>
 148    );
 149  }
 150