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