Fix Nostr login functionality
build-and-push / docker (push) Has been cancelled

- Fix NIP-07 extension login using NDKNip07Signer
- Fix NDK initialization to not block on relay connection
- Read relay URLs from VITE_DEFAULT_RELAYS env variable
- Add proper error handling for all login methods
- Remove NIP-55 option (Android only, not for web)
- Add vite-env.d.ts for TypeScript support
- Update .gitignore to exclude dist/ and .env
This commit is contained in:
root
2025-12-12 17:38:32 +00:00
parent 53db52e199
commit bda510ab49
12 changed files with 157 additions and 511 deletions
+35
View File
@@ -0,0 +1,35 @@
# Dependencies
node_modules/
# Build output
dist/
# Environment variables
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# OS files
.DS_Store
Thumbs.db
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Cache
.cache/
*.tsbuildinfo
File diff suppressed because one or more lines are too long
-189
View File
File diff suppressed because one or more lines are too long
-81
View File
File diff suppressed because one or more lines are too long
-59
View File
File diff suppressed because one or more lines are too long
-20
View File
@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NostrCount - Track Life Milestones</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-91eab0ce.js"></script>
<link rel="modulepreload" crossorigin href="/assets/ndk-40656944.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-beb84f6c.js">
<link rel="stylesheet" href="/assets/index-0ed31904.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v18h18"/>
<path d="M18 17V9"/>
<path d="M13 17V5"/>
<path d="M8 17v-3"/>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

+45 -82
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { X, Key, Zap, Plus, Copy, ExternalLink } from 'lucide-react'; import { X, Key, Zap, Plus, Copy } from 'lucide-react';
import { useNDK } from '../contexts/NDKContext'; import { useNDK } from '../contexts/NDKContext';
import { nip19, generateSecretKey, getPublicKey } from 'nostr-tools'; import { nip19, generateSecretKey, getPublicKey } from 'nostr-tools';
@@ -8,11 +8,11 @@ interface LoginModalProps {
onClose: () => void; onClose: () => void;
} }
type LoginMethod = 'extension' | 'keys' | 'create' | 'nip55'; type LoginMethod = 'extension' | 'keys' | 'create';
type CreateStep = 'username' | 'keys'; type CreateStep = 'username' | 'keys';
export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => { export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
const { login, loginWithKeys, loginWithNip55, updateUserProfile } = useNDK(); const { login, loginWithKeys, updateUserProfile, ndk } = useNDK();
const [loginMethod, setLoginMethod] = useState<LoginMethod>('extension'); const [loginMethod, setLoginMethod] = useState<LoginMethod>('extension');
const [createStep, setCreateStep] = useState<CreateStep>('username'); const [createStep, setCreateStep] = useState<CreateStep>('username');
@@ -25,11 +25,11 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
const [newPrivateKey, setNewPrivateKey] = useState(''); const [newPrivateKey, setNewPrivateKey] = useState('');
const [newPublicKey, setNewPublicKey] = useState(''); const [newPublicKey, setNewPublicKey] = useState('');
// NIP-55
const [nip55Url, setNip55Url] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Check if NDK is ready - just need the ndk instance, not waiting for all relays
const isNdkReady = ndk !== null;
const detectKeyType = (input: string): 'npub' | 'nsec' | 'unknown' => { const detectKeyType = (input: string): 'npub' | 'nsec' | 'unknown' => {
if (input.startsWith('npub')) return 'npub'; if (input.startsWith('npub')) return 'npub';
@@ -124,11 +124,16 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
// Login with the new keys // Login with the new keys
await loginWithKeys(newPublicKey, newPrivateKey); await loginWithKeys(newPublicKey, newPrivateKey);
// Update user profile with the username // Try to update user profile with the username, but don't fail if it doesn't work
await updateUserProfile({ try {
name: username, await updateUserProfile({
display_name: username, name: username,
}); display_name: username,
});
} catch (profileErr) {
console.warn('Could not update profile, but login was successful:', profileErr);
// Don't show error to user - the login worked, profile update is optional
}
onClose(); onClose();
} catch (err) { } catch (err) {
@@ -139,25 +144,6 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
} }
}; };
const handleNip55Login = async () => {
if (!nip55Url.trim()) {
setError('Please enter a NIP-55 URL');
return;
}
try {
setLoading(true);
setError(null);
await loginWithNip55(nip55Url);
onClose();
} catch (err) {
console.error('NIP-55 login failed:', err);
setError(err instanceof Error ? err.message : 'Login failed');
} finally {
setLoading(false);
}
};
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
}; };
@@ -168,7 +154,6 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
setUsername(''); setUsername('');
setNewPrivateKey(''); setNewPrivateKey('');
setNewPublicKey(''); setNewPublicKey('');
setNip55Url('');
setError(null); setError(null);
setCreateStep('username'); setCreateStep('username');
}; };
@@ -197,6 +182,15 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
</div> </div>
<div className="p-6"> <div className="p-6">
{/* Show warning if NDK is not ready */}
{!isNdkReady && (
<div className="mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p className="text-yellow-700 text-sm">
Connecting to Nostr relays... Please wait.
</p>
</div>
)}
<div className="flex gap-2 mb-6"> <div className="flex gap-2 mb-6">
<button <button
onClick={() => setLoginMethod('extension')} onClick={() => setLoginMethod('extension')}
@@ -231,17 +225,6 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<Plus className="w-4 h-4 inline mr-1" /> <Plus className="w-4 h-4 inline mr-1" />
Create Create
</button> </button>
<button
onClick={() => setLoginMethod('nip55')}
className={`flex-1 px-3 py-2 rounded-md font-medium transition-colors text-sm ${
loginMethod === 'nip55'
? 'bg-blue-600 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
<ExternalLink className="w-4 h-4 inline mr-1" />
NIP-55
</button>
</div> </div>
{loginMethod === 'extension' && ( {loginMethod === 'extension' && (
@@ -249,12 +232,19 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<p className="text-gray-600 mb-4"> <p className="text-gray-600 mb-4">
Connect using your Nostr browser extension (Alby, nos2x, etc.) Connect using your Nostr browser extension (Alby, nos2x, etc.)
</p> </p>
{typeof window !== 'undefined' && !window.nostr && (
<div className="mb-4 p-3 bg-orange-50 border border-orange-200 rounded-md">
<p className="text-orange-700 text-sm">
No Nostr extension detected. Install <a href="https://getalby.com" target="_blank" rel="noopener noreferrer" className="underline">Alby</a> or another NIP-07 extension.
</p>
</div>
)}
<button <button
onClick={handleExtensionLogin} onClick={handleExtensionLogin}
disabled={loading} disabled={loading || !isNdkReady}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50" className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
> >
{loading ? 'Connecting...' : 'Connect Extension'} {loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'Connect Extension'}
</button> </button>
</div> </div>
)} )}
@@ -263,29 +253,29 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Public or Private Key Private Key (nsec)
</label> </label>
<input <input
type="password" type="password"
value={keyInput} value={keyInput}
onChange={(e) => handleKeyInputChange(e.target.value)} onChange={(e) => handleKeyInputChange(e.target.value)}
placeholder="npub1... or nsec1..." placeholder="nsec1..."
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading} disabled={loading}
/> />
{keyType !== 'unknown' && ( {keyType !== 'unknown' && (
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Detected: {keyType === 'npub' ? 'Public Key (Read-only)' : 'Private Key (Full Access)'} Detected: {keyType === 'npub' ? 'Public Key (Read-only - not supported)' : 'Private Key (Full Access)'}
</p> </p>
)} )}
</div> </div>
<button <button
onClick={handleKeysLogin} onClick={handleKeysLogin}
disabled={loading || keyType === 'unknown'} disabled={loading || keyType === 'unknown' || keyType === 'npub' || !isNdkReady}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50" className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
> >
{loading ? 'Connecting...' : 'Connect with Keys'} {loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'Connect with Keys'}
</button> </button>
</div> </div>
)} )}
@@ -322,9 +312,9 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
{loginMethod === 'create' && createStep === 'keys' && ( {loginMethod === 'create' && createStep === 'keys' && (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-yellow-50 border border-yellow-200 rounded-md p-4"> <div className="bg-yellow-50 border border-yellow-200 rounded-md p-4">
<h3 className="font-semibold text-yellow-800 mb-2">Save Your Keys!</h3> <h3 className="font-semibold text-yellow-800 mb-2"> Save Your Keys!</h3>
<p className="text-sm text-yellow-700 mb-3"> <p className="text-sm text-yellow-700 mb-3">
Store these keys safely. You'll need them to access your account. Store these keys safely. You'll need them to access your account. If you lose them, you lose access forever!
</p> </p>
</div> </div>
@@ -342,6 +332,7 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button <button
onClick={() => copyToClipboard(newPrivateKey)} onClick={() => copyToClipboard(newPrivateKey)}
className="absolute right-2 top-2 p-1 hover:bg-gray-200 rounded" className="absolute right-2 top-2 p-1 hover:bg-gray-200 rounded"
title="Copy to clipboard"
> >
<Copy className="w-4 h-4 text-gray-500" /> <Copy className="w-4 h-4 text-gray-500" />
</button> </button>
@@ -362,6 +353,7 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button <button
onClick={() => copyToClipboard(newPublicKey)} onClick={() => copyToClipboard(newPublicKey)}
className="absolute right-2 top-2 p-1 hover:bg-gray-200 rounded" className="absolute right-2 top-2 p-1 hover:bg-gray-200 rounded"
title="Copy to clipboard"
> >
<Copy className="w-4 h-4 text-gray-500" /> <Copy className="w-4 h-4 text-gray-500" />
</button> </button>
@@ -370,39 +362,10 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button <button
onClick={handleUseNewKeys} onClick={handleUseNewKeys}
disabled={loading} disabled={loading || !isNdkReady}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50" className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
> >
{loading ? 'Connecting...' : 'Use These Keys'} {loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'I Saved My Keys - Continue'}
</button>
</div>
)}
{loginMethod === 'nip55' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
NIP-55 URL
</label>
<input
type="url"
value={nip55Url}
onChange={(e) => setNip55Url(e.target.value)}
placeholder="https://example.com/.well-known/nostr.json"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
<p className="text-xs text-gray-500 mt-1">
Enter a NIP-55 compatible URL to connect with an external signer
</p>
</div>
<button
onClick={handleNip55Login}
disabled={loading || !nip55Url.trim()}
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
>
{loading ? 'Connecting...' : 'Connect with NIP-55'}
</button> </button>
</div> </div>
)} )}
+52 -65
View File
@@ -1,5 +1,5 @@
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
import NDK, { NDKEvent, NDKUser, NDKSigner, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import NDK, { NDKEvent, NDKUser, NDKPrivateKeySigner, NDKNip07Signer } from '@nostr-dev-kit/ndk';
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'; import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie';
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from 'nostr-tools';
import { DEFAULT_RELAYS } from '../utils/nostr'; import { DEFAULT_RELAYS } from '../utils/nostr';
@@ -44,7 +44,7 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
useEffect(() => { useEffect(() => {
const initNDK = async () => { const initNDK = async () => {
try { try {
console.log('Initializing NDK with Dexie cache...'); console.log('Initializing NDK with relays:', DEFAULT_RELAYS);
// Initialize Dexie cache adapter // Initialize Dexie cache adapter
const dexieAdapter = new NDKCacheAdapterDexie({ const dexieAdapter = new NDKCacheAdapterDexie({
@@ -56,11 +56,22 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
cacheAdapter: dexieAdapter, cacheAdapter: dexieAdapter,
}); });
console.log('Connecting to relays...'); // Set NDK instance immediately so UI can render
await ndkInstance.connect();
console.log('NDK connected successfully with cache');
setNdk(ndkInstance); setNdk(ndkInstance);
console.log('Connecting to relays...');
// Connect without waiting - NDK handles relay connections in background
ndkInstance.connect().then(() => {
console.log('NDK connect() completed');
}).catch((err) => {
console.warn('NDK connect() had issues:', err);
});
// Give relays a moment to connect, but don't block forever
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('NDK initialized, relay connections in progress');
setIsConnected(true);
setIsLoading(false); setIsLoading(false);
} catch (error) { } catch (error) {
console.error('Failed to initialize NDK:', error); console.error('Failed to initialize NDK:', error);
@@ -72,56 +83,39 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
}, []); }, []);
const login = async () => { const login = async () => {
if (!ndk) return; if (!ndk) {
throw new Error('NDK not initialized. Please wait for connection to relays.');
}
try { try {
setIsLoading(true); setIsLoading(true);
console.log('Attempting login with NIP-07 extension...'); console.log('Attempting login with NIP-07 extension...');
// Check for NIP-07 extension // Check for NIP-07 extension
if (window.nostr) { if (typeof window !== 'undefined' && window.nostr) {
console.log('Nostr extension found, getting public key...'); console.log('Nostr extension found, creating NIP-07 signer...');
const pubkey = await window.nostr.getPublicKey();
console.log('Got public key:', pubkey);
const ndkUser = ndk.getUser({ pubkey }); // Use NDK's built-in NIP-07 signer
const nip07Signer = new NDKNip07Signer();
ndk.signer = nip07Signer;
// Set signer // Wait for the signer to be ready and get the user
ndk.signer = { const ndkUser = await nip07Signer.user();
user: async () => ndkUser, console.log('Got user from signer:', ndkUser.pubkey);
sign: async (event: NDKEvent) => {
console.log('Signing event with NIP-07 extension...');
if (!window.nostr) throw new Error('Nostr extension not available');
// Get the raw event data for signing
const rawEvent = {
kind: event.kind,
created_at: event.created_at || Math.floor(Date.now() / 1000),
tags: event.tags,
content: event.content,
pubkey: event.pubkey,
};
console.log('Raw event to sign:', rawEvent);
const signedEvent = await window.nostr.signEvent(rawEvent as any);
console.log('Event signed successfully:', signedEvent);
event.sig = signedEvent.sig;
return event.sig;
},
blockUntilReady: async () => true,
} as unknown as NDKSigner;
setUser(ndkUser); setUser(ndkUser);
setIsConnected(true); setIsConnected(true);
console.log('Login successful with NIP-07'); console.log('Login successful with NIP-07');
// Fetch user profile // Fetch user profile (don't fail if this fails)
const profile = await fetchUserProfile(pubkey); try {
setUserProfile(profile); const profile = await fetchUserProfile(ndkUser.pubkey);
setUserProfile(profile);
} catch (profileError) {
console.warn('Could not fetch profile:', profileError);
}
} else { } else {
throw new Error('No Nostr extension found'); throw new Error('No Nostr extension found. Please install a Nostr browser extension like Alby or nos2x.');
} }
} catch (error) { } catch (error) {
console.error('Login failed:', error); console.error('Login failed:', error);
@@ -132,7 +126,9 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
}; };
const loginWithKeys = async (npub: string, nsec: string) => { const loginWithKeys = async (npub: string, nsec: string) => {
if (!ndk) return; if (!ndk) {
throw new Error('NDK not initialized. Please wait for connection to relays.');
}
try { try {
setIsLoading(true); setIsLoading(true);
@@ -191,9 +187,13 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
setIsConnected(true); setIsConnected(true);
console.log('Login successful with keys'); console.log('Login successful with keys');
// Fetch user profile // Fetch user profile (don't fail if this fails)
const profile = await fetchUserProfile(pubkey); try {
setUserProfile(profile); const profile = await fetchUserProfile(pubkey);
setUserProfile(profile);
} catch (profileError) {
console.warn('Could not fetch profile:', profileError);
}
} catch (error) { } catch (error) {
console.error('Keys login failed:', error); console.error('Keys login failed:', error);
throw error; throw error;
@@ -202,26 +202,13 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
} }
}; };
const loginWithNip55 = async (url: string) => { const loginWithNip55 = async (_url: string) => {
if (!ndk) return; if (!ndk) {
throw new Error('NDK not initialized. Please wait for connection to relays.');
try {
setIsLoading(true);
console.log('Attempting NIP-55 login with URL:', url);
// TODO: Implement NIP-55 login
// This would involve:
// 1. Fetching the NIP-55 JSON from the URL
// 2. Validating the response
// 3. Setting up the signer for external signing
throw new Error('NIP-55 login not yet implemented');
} catch (error) {
console.error('NIP-55 login failed:', error);
throw error;
} finally {
setIsLoading(false);
} }
// NIP-55 is for Android Signer apps - not applicable for web browsers
throw new Error('NIP-55 is for Android signer apps. Please use the Extension or Keys login method instead.');
}; };
const logout = () => { const logout = () => {
+1 -1
View File
@@ -48,4 +48,4 @@ export interface RelayConfig {
url: string; url: string;
read: boolean; read: boolean;
write: boolean; write: boolean;
} }
+11 -7
View File
@@ -169,13 +169,17 @@ export function getCounterFilter(pubkey?: string, isPublic?: boolean): NDKFilter
return filter; return filter;
} }
export const DEFAULT_RELAYS = [ // Read relays from environment variable, fallback to defaults
'wss://relay.azzamo.net', const envRelays = import.meta.env.VITE_DEFAULT_RELAYS;
'wss://relay.damus.io', export const DEFAULT_RELAYS: string[] = envRelays
'wss://nostr.oxtr.dev', ? envRelays.split(',').map((r: string) => r.trim()).filter((r: string) => r.length > 0)
'wss://nos.lol', : [
'wss://relay.snort.social', 'wss://relay.azzamo.net',
]; 'wss://relay.damus.io',
'wss://nostr.oxtr.dev',
'wss://nos.lol',
'wss://relay.snort.social',
];
export function parseNip05(nip05: string): { name: string; domain: string } | null { export function parseNip05(nip05: string): { name: string; domain: string } | null {
if (!nip05 || !nip05.includes('@')) return null; if (!nip05 || !nip05.includes('@')) return null;
+13
View File
@@ -0,0 +1,13 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DEFAULT_RELAYS: string;
readonly VITE_APP_NAME: string;
readonly VITE_APP_DESCRIPTION: string;
readonly VITE_APP_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}