Fix Nostr login functionality
Some checks failed
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
.gitignore vendored Normal file
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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

20
dist/index.html vendored
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
dist/vite.svg vendored
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

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,12 +25,12 @@ 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';
if (input.startsWith('nsec')) return 'nsec'; if (input.startsWith('nsec')) return 'nsec';
@@ -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
try {
await updateUserProfile({ await updateUserProfile({
name: username, name: username,
display_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>
)} )}

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 {
const profile = await fetchUserProfile(ndkUser.pubkey);
setUserProfile(profile); 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)
try {
const profile = await fetchUserProfile(pubkey); const profile = await fetchUserProfile(pubkey);
setUserProfile(profile); 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 = () => {

View File

@@ -169,7 +169,11 @@ export function getCounterFilter(pubkey?: string, isPublic?: boolean): NDKFilter
return filter; return filter;
} }
export const DEFAULT_RELAYS = [ // Read relays from environment variable, fallback to defaults
const envRelays = import.meta.env.VITE_DEFAULT_RELAYS;
export const DEFAULT_RELAYS: string[] = envRelays
? envRelays.split(',').map((r: string) => r.trim()).filter((r: string) => r.length > 0)
: [
'wss://relay.azzamo.net', 'wss://relay.azzamo.net',
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://nostr.oxtr.dev', 'wss://nostr.oxtr.dev',

13
src/vite-env.d.ts vendored Normal file
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;
}