Merge pull request #473 from v0l/nip5-manager

Nip5 manager
This commit is contained in:
Kieran 2023-03-31 10:10:41 +01:00 committed by GitHub
commit 6fe19ee29d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 222 additions and 13 deletions

View File

@ -370,10 +370,11 @@ export default function useEventPublisher() {
publicKey: pubKey, publicKey: pubKey,
}; };
}, },
generic: async (content: string, kind: EventKind) => { generic: async (content: string, kind: EventKind, tags?: Array<Array<string>>) => {
if (pubKey) { if (pubKey) {
const ev = EventExt.forPubKey(pubKey, kind); const ev = EventExt.forPubKey(pubKey, kind);
ev.content = content; ev.content = content;
ev.tags = tags ?? [];
return await signEvent(ev); return await signEvent(ev);
} }
}, },

View File

@ -16,6 +16,7 @@ export type ServiceErrorCode =
export interface ServiceError { export interface ServiceError {
error: ServiceErrorCode; error: ServiceErrorCode;
errors: Array<string>;
} }
export interface ServiceConfig { export interface ServiceConfig {
@ -67,18 +68,18 @@ export class ServiceProvider {
} }
async GetConfig(): Promise<ServiceConfig | ServiceError> { async GetConfig(): Promise<ServiceConfig | ServiceError> {
return await this._GetJson("/config.json"); return await this.getJson("/config.json");
} }
async CheckAvailable(handle: string, domain: string): Promise<HandleAvailability | ServiceError> { async CheckAvailable(handle: string, domain: string): Promise<HandleAvailability | ServiceError> {
return await this._GetJson("/registration/availability", "POST", { return await this.getJson("/registration/availability", "POST", {
name: handle, name: handle,
domain, domain,
}); });
} }
async RegisterHandle(handle: string, domain: string, pubkey: string): Promise<HandleRegisterResponse | ServiceError> { async RegisterHandle(handle: string, domain: string, pubkey: string): Promise<HandleRegisterResponse | ServiceError> {
return await this._GetJson("/registration/register", "PUT", { return await this.getJson("/registration/register", "PUT", {
name: handle, name: handle,
domain, domain,
pk: pubkey, pk: pubkey,
@ -87,11 +88,12 @@ export class ServiceProvider {
} }
async CheckRegistration(token: string): Promise<CheckRegisterResponse | ServiceError> { async CheckRegistration(token: string): Promise<CheckRegisterResponse | ServiceError> {
return await this._GetJson("/registration/register/check", "POST", undefined, { return await this.getJson("/registration/register/check", "POST", undefined, {
authorization: token, authorization: token,
}); });
} }
async _GetJson<T>(
protected async getJson<T>(
path: string, path: string,
method?: "GET" | string, method?: "GET" | string,
body?: { [key: string]: string }, body?: { [key: string]: string },
@ -110,12 +112,12 @@ export class ServiceProvider {
const obj = await rsp.json(); const obj = await rsp.json();
if ("error" in obj) { if ("error" in obj) {
return <ServiceError>obj; return obj as ServiceError;
} }
return obj; return obj as T;
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
return { error: "UNKNOWN_ERROR" }; return { error: "UNKNOWN_ERROR", errors: [] };
} }
} }

View File

@ -0,0 +1,50 @@
import { EventKind } from "@snort/nostr";
import { EventPublisher } from "Feed/EventPublisher";
import { ServiceError, ServiceProvider } from "./ServiceProvider";
export interface ManageHandle {
id: string;
handle: string;
domain: string;
pubkey: string;
created: Date;
}
export default class SnortServiceProvider extends ServiceProvider {
readonly #publisher: EventPublisher;
constructor(publisher: EventPublisher, url: string | URL) {
super(url);
this.#publisher = publisher;
}
async list() {
return this.getJsonAuthd<Array<ManageHandle>>("/list", "GET");
}
async transfer(id: string, to: string) {
return this.getJsonAuthd<object>(`/${id}?to=${to}`, "PATCH");
}
async getJsonAuthd<T>(
path: string,
method?: "GET" | string,
body?: { [key: string]: string },
headers?: { [key: string]: string }
): Promise<T | ServiceError> {
const auth = await this.#publisher.generic("", EventKind.HttpAuthentication, [
["url", `${this.url}${path}`],
["method", method ?? "GET"],
]);
if (!auth) {
return {
error: "INVALID_TOKEN",
} as ServiceError;
}
return this.getJson<T>(path, method, body, {
...headers,
authorization: `Nostr ${window.btoa(JSON.stringify(auth))}`,
});
}
}

View File

@ -6,6 +6,7 @@ import Relay from "Pages/settings/Relays";
import Preferences from "Pages/settings/Preferences"; import Preferences from "Pages/settings/Preferences";
import RelayInfo from "Pages/settings/RelayInfo"; import RelayInfo from "Pages/settings/RelayInfo";
import { WalletSettingsRoutes } from "Pages/settings/WalletSettings"; import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
import Nip5ManagePage from "Pages/settings/ManageNip5";
import messages from "./messages"; import messages from "./messages";
@ -43,5 +44,9 @@ export const SettingsRoutes: RouteObject[] = [
path: "preferences", path: "preferences",
element: <Preferences />, element: <Preferences />,
}, },
{
path: "nip5",
element: <Nip5ManagePage />,
},
...WalletSettingsRoutes, ...WalletSettingsRoutes,
]; ];

View File

@ -47,6 +47,11 @@ const SettingsIndex = () => {
<FormattedMessage {...messages.Donate} /> <FormattedMessage {...messages.Donate} />
<Icon name="arrowFront" /> <Icon name="arrowFront" />
</div> </div>
<div className="settings-row" onClick={() => navigate("nip5")}>
<Icon name="badge" />
<FormattedMessage defaultMessage="Manage Nostr Adddress (NIP-05)" />
<Icon name="arrowFront" />
</div>
<div className="settings-row" onClick={handleLogout}> <div className="settings-row" onClick={handleLogout}>
<Icon name="logout" /> <Icon name="logout" />
<FormattedMessage {...messages.LogOut} /> <FormattedMessage {...messages.LogOut} />

View File

@ -0,0 +1,113 @@
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import { ApiHost } from "Const";
import Modal from "Element/Modal";
import useEventPublisher from "Feed/EventPublisher";
import { ServiceError } from "Nip05/ServiceProvider";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function Nip5ManagePage() {
const publisher = useEventPublisher();
const { formatMessage } = useIntl();
const [handles, setHandles] = useState<Array<ManageHandle>>();
const [transfer, setTransfer] = useState("");
const [newKey, setNewKey] = useState("");
const [error, setError] = useState<Array<string>>([]);
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
useEffect(() => {
loadHandles().catch(console.error);
}, []);
async function loadHandles() {
const list = await sp.list();
setHandles(list as Array<ManageHandle>);
}
async function startTransfer() {
if (!transfer || !newKey) return;
setError([]);
const rsp = await sp.transfer(transfer, newKey);
if ("error" in rsp) {
setError((rsp as ServiceError).errors);
return;
}
await loadHandles();
setTransfer("");
setNewKey("");
}
function close() {
setTransfer("");
setNewKey("");
setError([]);
}
if (!handles) {
return null;
}
return (
<>
<h3>
<FormattedMessage defaultMessage="Nostr Address" />
</h3>
{handles.length === 0 && (
<FormattedMessage
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<>
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button className="button" onClick={() => setTransfer(a.id)}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
</div>
</>
))}
{transfer && (
<Modal onClose={close}>
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>
<div className="flex">
<div className="f-grow">
<input
type="text"
className="w-max mr10"
placeholder={formatMessage({
defaultMessage: "Public key (npub/nprofile)",
})}
value={newKey}
onChange={e => setNewKey(e.target.value)}
/>
</div>
<button className="button" onClick={() => startTransfer()}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
{error && <b className="error">{error}</b>}
</Modal>
)}
</>
);
}

View File

@ -121,6 +121,9 @@
"5rOdPG": { "5rOdPG": {
"defaultMessage": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow." "defaultMessage": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow."
}, },
"5u6iEc": {
"defaultMessage": "Transfer to Pubkey"
},
"5ykRmX": { "5ykRmX": {
"defaultMessage": "Send zap" "defaultMessage": "Send zap"
}, },
@ -174,6 +177,9 @@
"defaultMessage": "Login", "defaultMessage": "Login",
"description": "Login button" "description": "Login button"
}, },
"9pMqYs": {
"defaultMessage": "Nostr Address"
},
"9wO4wJ": { "9wO4wJ": {
"defaultMessage": "Lightning Invoice" "defaultMessage": "Lightning Invoice"
}, },
@ -239,6 +245,9 @@
"Dt/Zd5": { "Dt/Zd5": {
"defaultMessage": "Media in posts will automatically be shown for selected people, otherwise only the link will show" "defaultMessage": "Media in posts will automatically be shown for selected people, otherwise only the link will show"
}, },
"DtYelJ": {
"defaultMessage": "Transfer"
},
"E8a4yq": { "E8a4yq": {
"defaultMessage": "Follow some popular accounts" "defaultMessage": "Follow some popular accounts"
}, },
@ -275,6 +284,9 @@
"FmXUJg": { "FmXUJg": {
"defaultMessage": "follows you" "defaultMessage": "follows you"
}, },
"FpxElY": {
"defaultMessage": "Verification"
},
"G/yZLu": { "G/yZLu": {
"defaultMessage": "Remove" "defaultMessage": "Remove"
}, },
@ -366,6 +378,9 @@
"LF5kYT": { "LF5kYT": {
"defaultMessage": "Other Connections" "defaultMessage": "Other Connections"
}, },
"LQahqW": {
"defaultMessage": "Manage Nostr Adddress (NIP-05)"
},
"LXxsbk": { "LXxsbk": {
"defaultMessage": "Anonymous" "defaultMessage": "Anonymous"
}, },
@ -482,6 +497,9 @@
"defaultMessage": "Hex Salt..", "defaultMessage": "Hex Salt..",
"description": "Hexidecimal 'salt' input for imgproxy" "description": "Hexidecimal 'salt' input for imgproxy"
}, },
"UDYlxu": {
"defaultMessage": "Pending Subscriptions"
},
"UQ3pOC": { "UQ3pOC": {
"defaultMessage": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step." "defaultMessage": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step."
}, },
@ -497,6 +515,9 @@
"VOjC1i": { "VOjC1i": {
"defaultMessage": "Pick which upload service you want to upload attachments to" "defaultMessage": "Pick which upload service you want to upload attachments to"
}, },
"VR5eHw": {
"defaultMessage": "Public key (npub/nprofile)"
},
"VlJkSk": { "VlJkSk": {
"defaultMessage": "{n} muted" "defaultMessage": "{n} muted"
}, },
@ -669,6 +690,9 @@
"iNWbVV": { "iNWbVV": {
"defaultMessage": "Handle" "defaultMessage": "Handle"
}, },
"iXPL0Z": {
"defaultMessage": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead"
},
"ieGrWo": { "ieGrWo": {
"defaultMessage": "Follow" "defaultMessage": "Follow"
}, },
@ -733,6 +757,9 @@
"lvlPhZ": { "lvlPhZ": {
"defaultMessage": "Pay Invoice" "defaultMessage": "Pay Invoice"
}, },
"mErPop": {
"defaultMessage": "It looks like you dont have any, check {link} to buy one!"
},
"mH91FY": { "mH91FY": {
"defaultMessage": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below" "defaultMessage": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below"
}, },
@ -861,9 +888,6 @@
"uSV4Ti": { "uSV4Ti": {
"defaultMessage": "Reposts need to be manually confirmed" "defaultMessage": "Reposts need to be manually confirmed"
}, },
"ubtr9S": {
"defaultMessage": "Can't login with private key on 'http://' connection, please use a Nostr key manager extension instead"
},
"usAvMr": { "usAvMr": {
"defaultMessage": "Edit Profile" "defaultMessage": "Edit Profile"
}, },

View File

@ -39,6 +39,7 @@
"4Z3t5i": "Use imgproxy to compress images", "4Z3t5i": "Use imgproxy to compress images",
"4rYCjn": "Note to Self", "4rYCjn": "Note to Self",
"5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.", "5rOdPG": "Once you setup your key manager extension and generated a key, you can follow our new users flow to setup your profile and help you find some interesting people on Nostr to follow.",
"5u6iEc": "Transfer to Pubkey",
"5ykRmX": "Send zap", "5ykRmX": "Send zap",
"6ewQqw": "Likes ({n})", "6ewQqw": "Likes ({n})",
"6tUqAb": "Generate a public / private key pair. Do not share your private key with anyone, this acts as your password. Once lost, it cannot be “reset” or recovered. Keep safe!", "6tUqAb": "Generate a public / private key pair. Do not share your private key with anyone, this acts as your password. Once lost, it cannot be “reset” or recovered. Keep safe!",
@ -56,6 +57,7 @@
"9SvQep": "Follows {n}", "9SvQep": "Follows {n}",
"9WRlF4": "Send", "9WRlF4": "Send",
"9gqH2W": "Login", "9gqH2W": "Login",
"9pMqYs": "Nostr Address",
"9wO4wJ": "Lightning Invoice", "9wO4wJ": "Lightning Invoice",
"ADmfQT": "Parent", "ADmfQT": "Parent",
"ASRK0S": "This author has been muted", "ASRK0S": "This author has been muted",
@ -77,6 +79,7 @@
"DZzCem": "Show latest {n} notes", "DZzCem": "Show latest {n} notes",
"Dh3hbq": "Auto Zap", "Dh3hbq": "Auto Zap",
"Dt/Zd5": "Media in posts will automatically be shown for selected people, otherwise only the link will show", "Dt/Zd5": "Media in posts will automatically be shown for selected people, otherwise only the link will show",
"DtYelJ": "Transfer",
"E8a4yq": "Follow some popular accounts", "E8a4yq": "Follow some popular accounts",
"EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.", "EPYwm7": "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.",
"EWyQH5": "Global", "EWyQH5": "Global",
@ -89,6 +92,7 @@
"FS3b54": "Done!", "FS3b54": "Done!",
"FfYsOb": "An error has occured!", "FfYsOb": "An error has occured!",
"FmXUJg": "follows you", "FmXUJg": "follows you",
"FpxElY": "Verification",
"G/yZLu": "Remove", "G/yZLu": "Remove",
"G1BGCg": "Select Wallet", "G1BGCg": "Select Wallet",
"GFOoEE": "Salt", "GFOoEE": "Salt",
@ -119,6 +123,7 @@
"KahimY": "Unknown event kind: {kind}", "KahimY": "Unknown event kind: {kind}",
"L7SZPr": "For more information about donations see {link}.", "L7SZPr": "For more information about donations see {link}.",
"LF5kYT": "Other Connections", "LF5kYT": "Other Connections",
"LQahqW": "Manage Nostr Adddress (NIP-05)",
"LXxsbk": "Anonymous", "LXxsbk": "Anonymous",
"LgbKvU": "Comment", "LgbKvU": "Comment",
"LxY9tW": "Generate Key", "LxY9tW": "Generate Key",
@ -156,11 +161,13 @@
"Rs4kCE": "Bookmark", "Rs4kCE": "Bookmark",
"Sjo1P4": "Custom", "Sjo1P4": "Custom",
"TpgeGw": "Hex Salt..", "TpgeGw": "Hex Salt..",
"UDYlxu": "Pending Subscriptions",
"UQ3pOC": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.", "UQ3pOC": "On Nostr, many people have the same username. User names and identity are separate things. You can get a unique identifier in the next step.",
"Up5U7K": "Block", "Up5U7K": "Block",
"VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.", "VBadwB": "Hmm, can't find a key manager extension.. try reloading the page.",
"VN0+Fz": "Balance: {amount} sats", "VN0+Fz": "Balance: {amount} sats",
"VOjC1i": "Pick which upload service you want to upload attachments to", "VOjC1i": "Pick which upload service you want to upload attachments to",
"VR5eHw": "Public key (npub/nprofile)",
"VlJkSk": "{n} muted", "VlJkSk": "{n} muted",
"VnXp8Z": "Avatar", "VnXp8Z": "Avatar",
"VtPV/B": "Login with Extension (NIP-07)", "VtPV/B": "Login with Extension (NIP-07)",
@ -217,6 +224,7 @@
"iDGAbc": "Get a Snort identifier", "iDGAbc": "Get a Snort identifier",
"iGT1eE": "Prevent fake accounts from imitating you", "iGT1eE": "Prevent fake accounts from imitating you",
"iNWbVV": "Handle", "iNWbVV": "Handle",
"iXPL0Z": "Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
"ieGrWo": "Follow", "ieGrWo": "Follow",
"itPgxd": "Profile", "itPgxd": "Profile",
"izWS4J": "Unfollow", "izWS4J": "Unfollow",
@ -238,6 +246,7 @@
"lnaT9F": "Following {n}", "lnaT9F": "Following {n}",
"lsNFM1": "Click to load content from {link}", "lsNFM1": "Click to load content from {link}",
"lvlPhZ": "Pay Invoice", "lvlPhZ": "Pay Invoice",
"mErPop": "It looks like you dont have any, check {link} to buy one!",
"mH91FY": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below", "mH91FY": "Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below",
"mKAr6h": "Follow all", "mKAr6h": "Follow all",
"mKh2HS": "File upload service", "mKh2HS": "File upload service",
@ -280,7 +289,6 @@
"u4bHcR": "Check out the code here: {link}", "u4bHcR": "Check out the code here: {link}",
"uD/N6c": "Zap {target} {n} sats", "uD/N6c": "Zap {target} {n} sats",
"uSV4Ti": "Reposts need to be manually confirmed", "uSV4Ti": "Reposts need to be manually confirmed",
"ubtr9S": "Can't login with private key on 'http://' connection, please use a Nostr key manager extension instead",
"usAvMr": "Edit Profile", "usAvMr": "Edit Profile",
"ut+2Cd": "Get a partner identifier", "ut+2Cd": "Get a partner identifier",
"vOKedj": "{n,plural,=1{& {n} other} other{& {n} others}}", "vOKedj": "{n,plural,=1{& {n} other} other{& {n} others}}",

View File

@ -19,6 +19,7 @@ enum EventKind {
ProfileBadges = 30008, // NIP-58 ProfileBadges = 30008, // NIP-58
ZapRequest = 9734, // NIP 57 ZapRequest = 9734, // NIP 57
ZapReceipt = 9735, // NIP 57 ZapReceipt = 9735, // NIP 57
HttpAuthentication = 27235, // NIP XX - HTTP Authentication
} }
export default EventKind; export default EventKind;