added login with nip05

This commit is contained in:
hzrd149
2023-02-14 08:06:07 -06:00
parent 5b301664a6
commit 8b660b4697
22 changed files with 206 additions and 62 deletions

View File

@@ -6,7 +6,7 @@ import { Page } from "./components/page";
import { SettingsView } from "./views/settings";
import { LoginView } from "./views/login";
import { ProfileView } from "./views/profile";
import identityService from "./services/identity";
import accountService from "./services/account";
import { FollowingTab } from "./views/home/following-tab";
import { DiscoverTab } from "./views/home/discover-tab";
import { GlobalTab } from "./views/home/global-tab";
@@ -23,10 +23,11 @@ import { LoginNpubView } from "./views/login/npub";
import NotificationsView from "./views/notifications";
import { RelaysView } from "./views/relays";
import useSubject from "./hooks/use-subject";
import { LoginNip05View } from "./views/login/nip05";
const RequireSetup = ({ children }: { children: JSX.Element }) => {
let location = useLocation();
const setup = useSubject(identityService.setup);
const setup = useSubject(accountService.setup);
if (!setup) return <Navigate to="/login" state={{ from: location.pathname }} replace />;
@@ -48,6 +49,7 @@ const router = createBrowserRouter([
children: [
{ path: "", element: <LoginStartView /> },
{ path: "npub", element: <LoginNpubView /> },
{ path: "nip05", element: <LoginNip05View /> },
],
},
{

View File

@@ -8,7 +8,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { NoteContents } from "./note-contents";
import { NoteMenu } from "./note-menu";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { UserTipButton } from "../user-tip-button";
import { NoteRelays } from "./note-relays";
@@ -31,7 +31,7 @@ export const Note = React.memo(({ event, maxHeight }: NoteProps) => {
const readonly = useReadonlyMode();
const { openModal } = useContext(PostModalContext);
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const contacts = useUserContacts(pubkey);
const following = contacts?.contacts || [];

View File

@@ -6,7 +6,7 @@ import { ErrorBoundary } from "./error-boundary";
import { ConnectedRelays } from "./connected-relays";
import { useIsMobile } from "../hooks/use-is-mobile";
import identityService from "../services/identity";
import accountService from "../services/account";
import { FollowingList } from "./following-list";
import { ReloadPrompt } from "./reload-prompt";
import { PostModalProvider } from "../providers/post-modal-provider";
@@ -16,7 +16,7 @@ import { UserAvatarLink } from "./user-avatar-link";
import useSubject from "../hooks/use-subject";
const MobileProfileHeader = () => {
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const readonly = useReadonlyMode();
return (
@@ -27,7 +27,7 @@ const MobileProfileHeader = () => {
colorScheme="red"
textAlign="center"
variant="link"
onClick={() => confirm("Exit readonly mode?") && identityService.logout()}
onClick={() => confirm("Exit readonly mode?") && accountService.logout()}
>
Readonly Mode
</Button>
@@ -96,7 +96,7 @@ const DesktopSideNav = () => {
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
Settings
</Button>
<Button onClick={() => identityService.logout()} leftIcon={<LogoutIcon />}>
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon />}>
Logout
</Button>
{readonly && (

View File

@@ -1,6 +1,6 @@
import { Box, LinkBox, Text } from "@chakra-ui/react";
import { Link } from "react-router-dom";
import identityService from "../services/identity";
import accountService from "../services/account";
import { UserAvatar } from "./user-avatar";
import { useUserMetadata } from "../hooks/use-user-metadata";
import { normalizeToBech32 } from "../helpers/nip-19";
@@ -8,7 +8,7 @@ import { truncatedId } from "../helpers/nostr-event";
import useSubject from "../hooks/use-subject";
export const ProfileButton = () => {
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const metadata = useUserMetadata(pubkey);
return (

View File

@@ -1,6 +1,6 @@
import identityService from "../services/identity";
import accountService from "../services/account";
import useSubject from "./use-subject";
export function useReadonlyMode() {
return useSubject(identityService.readonly);
return useSubject(accountService.readonly);
}

View File

@@ -9,7 +9,7 @@ export type SavedIdentity = {
useExtension: boolean;
};
class IdentityService {
class AccountService {
loading = new PersistentSubject(false);
setup = new PersistentSubject(false);
pubkey = new Subject<string>();
@@ -81,11 +81,11 @@ class IdentityService {
}
}
const identityService = new IdentityService();
const accountService = new AccountService();
if (import.meta.env.DEV) {
// @ts-ignore
window.identity = identityService;
window.identity = accountService;
}
export default identityService;
export default accountService;

View File

@@ -3,7 +3,7 @@ import { nostrPostAction } from "../classes/nostr-post-action";
import { PersistentSubject, Subject } from "../classes/subject";
import { DraftNostrEvent, PTag } from "../types/nostr-event";
import clientRelaysService from "./client-relays";
import identityService from "./identity";
import accountService from "./account";
import userContactsService, { UserContacts } from "./user-contacts";
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
@@ -34,14 +34,14 @@ function updateSub() {
sub = undefined;
}
if (identityService.pubkey.value) {
sub = userContactsService.requestContacts(identityService.pubkey.value, clientRelaysService.getReadUrls(), true);
if (accountService.pubkey.value) {
sub = userContactsService.requestContacts(accountService.pubkey.value, clientRelaysService.getReadUrls(), true);
sub.subscribe(handleNewContacts);
}
}
identityService.pubkey.subscribe(() => {
accountService.pubkey.subscribe(() => {
// clear the following list until a new one can be fetched
following.next([]);

View File

@@ -2,7 +2,7 @@ import moment from "moment";
import { nostrPostAction } from "../classes/nostr-post-action";
import { unique } from "../helpers/array";
import { DraftNostrEvent, RTag } from "../types/nostr-event";
import identityService from "./identity";
import accountService from "./account";
import { RelayConfig, RelayMode } from "../classes/relay";
import userRelaysService, { UserRelays } from "./user-relays";
import { PersistentSubject, Subject } from "../classes/subject";
@@ -23,7 +23,7 @@ class ClientRelayService {
constructor() {
let lastSubject: Subject<UserRelays> | undefined;
identityService.pubkey.subscribe((pubkey) => {
accountService.pubkey.subscribe((pubkey) => {
// clear the relay list until a new one can be fetched
// this.relays.next([]);
@@ -38,7 +38,7 @@ class ClientRelayService {
});
// add preset relays fromm nip07 extension to bootstrap list
identityService.relays.subscribe((presetRelays) => {
accountService.relays.subscribe((presetRelays) => {
for (const [url, opts] of Object.entries(presetRelays)) {
if (opts.read) {
clientRelaysService.bootstrapRelays.add(url);

View File

@@ -1,7 +1,7 @@
import moment from "moment";
import db from "./db";
function parseAddress(address: string) {
function parseAddress(address: string): { name?: string; domain?: string } {
const parts = address.split("@");
return { name: parts[0], domain: parts[1] };
}
@@ -18,10 +18,11 @@ export type DnsIdentity = {
};
function getIdentityFromJson(name: string, domain: string, json: IdentityJson): DnsIdentity | undefined {
const relays: string[] = json.relays?.[name] ?? [];
const pubkey = json.names[name];
if (!pubkey) return;
if (pubkey) return { name, domain, pubkey, relays };
const relays: string[] = json.relays?.[pubkey] ?? [];
return { name, domain, pubkey, relays };
}
async function fetchAllIdentities(domain: string) {
@@ -32,6 +33,7 @@ async function fetchAllIdentities(domain: string) {
async function fetchIdentity(address: string) {
const { name, domain } = parseAddress(address);
if (!name || !domain) return undefined;
const json = await fetch(`https://${domain}/.well-known/nostr.json?name=${name}`).then(
(res) => res.json() as Promise<IdentityJson>
);
@@ -53,9 +55,9 @@ async function addToCache(domain: string, json: IdentityJson) {
await Promise.all(wait);
}
async function getIdentity(address: string) {
async function getIdentity(address: string, alwaysFetch = false) {
const cached = await db.get("dnsIdentifiers", address);
if (cached) return cached;
if (cached && !alwaysFetch) return cached;
// TODO: if it fails, maybe cache a failure message
return fetchIdentity(address);
@@ -74,9 +76,9 @@ async function pruneCache() {
}
const pending: Record<string, ReturnType<typeof getIdentity> | undefined> = {};
function dedupedGetIdentity(address: string) {
function dedupedGetIdentity(address: string, alwaysFetch = false) {
if (pending[address]) return pending[address];
return (pending[address] = getIdentity(address));
return (pending[address] = getIdentity(address, alwaysFetch));
}
export const dnsIdentityService = {

View File

@@ -1,6 +1,6 @@
import { PersistentSubject } from "../classes/subject";
import db from "./db";
import { SavedIdentity } from "./identity";
import { SavedIdentity } from "./account";
const settings = {
identity: new PersistentSubject<SavedIdentity | null>(null),

View File

@@ -3,7 +3,7 @@ import { Button, Flex, Spinner } from "@chakra-ui/react";
import moment from "moment";
import { Note } from "../../components/note";
import { useUserContacts } from "../../hooks/use-user-contacts";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import userContactsService from "../../services/user-contacts";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { isNote } from "../../helpers/nostr-event";
@@ -41,7 +41,7 @@ function useExtendedContacts(pubkey: string) {
export const DiscoverTab = () => {
useAppTitle("discover");
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const relays = useReadRelayUrls();
const contactsOfContacts = useExtendedContacts(pubkey);

View File

@@ -5,7 +5,7 @@ import { Note } from "../../components/note";
import { isNote } from "../../helpers/nostr-event";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import { useUserContacts } from "../../hooks/use-user-contacts";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import { AddIcon } from "@chakra-ui/icons";
import { useContext } from "react";
import { PostModalContext } from "../../providers/post-modal-provider";
@@ -15,7 +15,7 @@ import useSubject from "../../hooks/use-subject";
export const FollowingTab = () => {
const readonly = useReadonlyMode();
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const relays = useReadRelayUrls();
const { openModal } = useContext(PostModalContext);
const contacts = useUserContacts(pubkey);

View File

@@ -1,10 +1,10 @@
import { Avatar, Box, Flex, Heading } from "@chakra-ui/react";
import { Navigate, Outlet, useLocation } from "react-router-dom";
import useSubject from "../../hooks/use-subject";
import identityService from "../../services/identity";
import accountService from "../../services/account";
export const LoginView = () => {
const setup = useSubject(identityService.setup);
const setup = useSubject(accountService.setup);
const location = useLocation();
if (setup) return <Navigate to={location.state?.from ?? "/"} replace />;

137
src/views/login/nip05.tsx Normal file
View File

@@ -0,0 +1,137 @@
import React, { useState } from "react";
import {
Button,
Flex,
FormControl,
FormHelperText,
FormLabel,
Input,
InputGroup,
InputRightElement,
Link,
Spinner,
Text,
useToast,
} from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import { RelayUrlInput } from "../../components/relay-url-input";
import { normalizeToHex } from "../../helpers/nip-19";
import accountService from "../../services/account";
import clientRelaysService from "../../services/client-relays";
import { useDebounce } from "react-use";
import dnsIdentityService from "../../services/dns-identity";
import { CheckIcon } from "../../components/icons";
import { CloseIcon } from "@chakra-ui/icons";
export const LoginNip05View = () => {
const navigate = useNavigate();
const toast = useToast();
const [loading, setLoading] = useState(false);
const [nip05, setNip05] = useState("");
const [relayUrl, setRelayUrl] = useState("");
const [pubkey, setPubkey] = useState<string | undefined>();
const [relays, setRelays] = useState<string[] | undefined>();
const handleNip05Change: React.ChangeEventHandler<HTMLInputElement> = (event) => {
setNip05(event.target.value);
setPubkey(undefined);
setRelays(undefined);
if (event.target.value) setLoading(true);
};
useDebounce(
async () => {
if (nip05) {
try {
const id = await dnsIdentityService.getIdentity(nip05, true);
setPubkey(id?.pubkey);
setRelays(id?.relays);
} catch (e) {}
}
setLoading(false);
},
1000,
[nip05, setPubkey, setRelays, setLoading]
);
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
e.preventDefault();
if (!pubkey) return toast({ status: "error", title: "Invalid NIP-05 id" });
if ((!relays || relays.length === 0) && !relayUrl) {
return toast({ status: "error", title: "No relay selected" });
}
accountService.loginWithPubkey(pubkey);
if (relayUrl) {
clientRelaysService.bootstrapRelays.add(relayUrl);
}
if (relays) {
for (const url of relays) {
clientRelaysService.bootstrapRelays.add(url);
}
}
};
const renderInputIcon = () => {
if (loading) return <Spinner size="sm" />;
if (nip05) {
if (pubkey) return <CheckIcon color="green.500" />;
else return <CloseIcon color="red.500" />;
}
return null;
};
return (
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="400">
<FormControl>
<FormLabel>Enter user NIP-05 id</FormLabel>
<InputGroup>
<Input
name="nip05"
placeholder="user@domain.com"
isRequired
value={nip05}
onChange={handleNip05Change}
colorScheme={"green"}
errorBorderColor={nip05 && !pubkey ? "red.500" : undefined}
/>
<InputRightElement children={renderInputIcon()} />
</InputGroup>
<FormHelperText>
Find NIP-05 ids here.{" "}
<Link isExternal href="https://nostrplebs.com/directory" color="blue.500" target="_blank">
nostrplebs.com/directory
</Link>
</FormHelperText>
</FormControl>
{relays ? (
relays.length > 0 ? (
<Text>Found {relays.length} relays</Text>
) : (
<FormControl>
<FormLabel>Bootstrap relay</FormLabel>
<RelayUrlInput
placeholder="wss://nostr.example.com"
isRequired
value={relayUrl}
onChange={(e) => setRelayUrl(e.target.value)}
/>
<FormHelperText>The first relay to connect to.</FormHelperText>
</FormControl>
)
) : null}
<Flex justifyContent="space-between" gap="2">
<Button variant="link" onClick={() => navigate("../")}>
Back
</Button>
<Button colorScheme="brand" ml="auto" type="submit" isDisabled={!pubkey}>
Login
</Button>
</Flex>
</Flex>
);
};

View File

@@ -3,7 +3,7 @@ import { Button, Flex, FormControl, FormHelperText, FormLabel, Input, Link, useT
import { useNavigate } from "react-router-dom";
import { RelayUrlInput } from "../../components/relay-url-input";
import { normalizeToHex } from "../../helpers/nip-19";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import clientRelaysService from "../../services/client-relays";
export const LoginNpubView = () => {
@@ -20,16 +20,13 @@ export const LoginNpubView = () => {
return toast({ status: "error", title: "Invalid npub" });
}
identityService.loginWithPubkey(pubkey);
accountService.loginWithPubkey(pubkey);
clientRelaysService.bootstrapRelays.add(relayUrl);
};
return (
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit}>
<Button variant="link" onClick={() => navigate("../")}>
Back
</Button>
<Flex as="form" direction="column" gap="4" onSubmit={handleSubmit} minWidth="400">
<FormControl>
<FormLabel>Enter user npub</FormLabel>
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />
@@ -50,9 +47,14 @@ export const LoginNpubView = () => {
/>
<FormHelperText>The first relay to connect to.</FormHelperText>
</FormControl>
<Button colorScheme="brand" ml="auto" type="submit">
Login
</Button>
<Flex justifyContent="space-between" gap="2">
<Button variant="link" onClick={() => navigate("../")}>
Back
</Button>
<Button colorScheme="brand" ml="auto" type="submit">
Login
</Button>
</Flex>
</Flex>
);
};

View File

@@ -1,11 +1,11 @@
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Button, Spinner } from "@chakra-ui/react";
import { useNavigate } from "react-router-dom";
import useSubject from "../../hooks/use-subject";
import identityService from "../../services/identity";
import accountService from "../../services/account";
export const LoginStartView = () => {
const navigate = useNavigate();
const loading = useSubject(identityService.loading);
const loading = useSubject(accountService.loading);
if (loading) return <Spinner />;
return (
@@ -17,9 +17,10 @@ export const LoginStartView = () => {
<AlertDescription>There are bugs and things will break.</AlertDescription>
</Box>
</Alert>
<Button onClick={() => identityService.loginWithExtension()} colorScheme="brand">
<Button onClick={() => accountService.loginWithExtension()} colorScheme="brand">
Use browser extension
</Button>
<Button onClick={() => navigate("./nip05")}>Login with Nip-05 Id</Button>
<Button variant="link" onClick={() => navigate("./npub")}>
Login with npub
</Button>

View File

@@ -8,7 +8,7 @@ import { convertTimestampToDate } from "../../helpers/date";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubject from "../../hooks/use-subject";
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import { NostrEvent } from "../../types/nostr-event";
const Kind1Notification = ({ event }: { event: NostrEvent }) => {
@@ -39,7 +39,7 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
const NotificationsView = () => {
const readRelays = useReadRelayUrls();
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const { events, loading, loadMore } = useTimelineLoader(
"notifications",
readRelays,

View File

@@ -3,7 +3,7 @@ import { useMemo } from "react";
import { useForm } from "react-hook-form";
import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import identityService from "../../services/identity";
import accountService from "../../services/account";
type FormData = {
displayName?: string;
@@ -60,7 +60,7 @@ const MetadataForm = ({ defaultValues, onSubmit }: MetadataFormProps) => {
};
export const ProfileEditView = () => {
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
const metadata = useUserMetadata(pubkey);
const defaultValues = useMemo<FormData>(

View File

@@ -1,9 +1,9 @@
import useSubject from "../../hooks/use-subject";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import { ProfileEditView } from "./edit";
export const ProfileView = () => {
const pubkey = useSubject(identityService.pubkey) ?? "";
const pubkey = useSubject(accountService.pubkey) ?? "";
return <ProfileEditView />;
};

View File

@@ -17,7 +17,7 @@ import {
import { useState } from "react";
import settings from "../../services/settings";
import { clearCacheData, deleteDatabase } from "../../services/db";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import useSubject from "../../hooks/use-subject";
export const SettingsView = () => {
@@ -171,7 +171,7 @@ export const SettingsView = () => {
</AccordionItem>
</Accordion>
<Flex gap="2" padding="4">
<Button onClick={() => identityService.logout()}>Logout</Button>
<Button onClick={() => accountService.logout()}>Logout</Button>
</Flex>
</Flex>
);

View File

@@ -3,7 +3,7 @@ import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-ic
import { IMAGE_ICONS, SpyIcon } from "../../../components/icons";
import { Bech32Prefix, normalizeToBech32 } from "../../../helpers/nip-19";
import identityService from "../../../services/identity";
import accountService from "../../../services/account";
import { useUserMetadata } from "../../../hooks/use-user-metadata";
import { getUserDisplayName } from "../../../helpers/user-metadata";
@@ -13,8 +13,8 @@ export const UserProfileMenu = ({ pubkey, ...props }: { pubkey: string } & Omit<
const loginAsUser = () => {
if (confirm(`Do you want to logout and login as ${getUserDisplayName(metadata, pubkey)}?`)) {
identityService.logout();
identityService.loginWithPubkey(pubkey);
accountService.logout();
accountService.loginWithPubkey(pubkey);
}
};

View File

@@ -24,7 +24,7 @@ import { truncatedId } from "../../helpers/nostr-event";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip-19";
import { KeyIcon, SettingsIcon } from "../../components/icons";
import { CopyIconButton } from "../../components/copy-icon-button";
import identityService from "../../services/identity";
import accountService from "../../services/account";
import { UserFollowButton } from "../../components/user-follow-button";
import { useAppTitle } from "../../hooks/use-app-title";
@@ -48,7 +48,7 @@ const UserView = () => {
const metadata = useUserMetadata(pubkey, [], true);
const npub = normalizeToBech32(pubkey, Bech32Prefix.Pubkey);
const isSelf = pubkey === identityService.pubkey.value;
const isSelf = pubkey === accountService.pubkey.value;
useAppTitle(getUserDisplayName(metadata, npub ?? pubkey));