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