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 { X, Key, Zap, Plus, Copy, ExternalLink } from 'lucide-react';
import { X, Key, Zap, Plus, Copy } from 'lucide-react';
import { useNDK } from '../contexts/NDKContext';
import { nip19, generateSecretKey, getPublicKey } from 'nostr-tools';
@@ -8,11 +8,11 @@ interface LoginModalProps {
onClose: () => void;
}
type LoginMethod = 'extension' | 'keys' | 'create' | 'nip55';
type LoginMethod = 'extension' | 'keys' | 'create';
type CreateStep = 'username' | 'keys';
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 [createStep, setCreateStep] = useState<CreateStep>('username');
@@ -25,12 +25,12 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
const [newPrivateKey, setNewPrivateKey] = useState('');
const [newPublicKey, setNewPublicKey] = useState('');
// NIP-55
const [nip55Url, setNip55Url] = useState('');
const [loading, setLoading] = useState(false);
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' => {
if (input.startsWith('npub')) return 'npub';
if (input.startsWith('nsec')) return 'nsec';
@@ -124,11 +124,16 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
// Login with the new keys
await loginWithKeys(newPublicKey, newPrivateKey);
// Update user profile with the username
await updateUserProfile({
name: username,
display_name: username,
});
// Try to update user profile with the username, but don't fail if it doesn't work
try {
await updateUserProfile({
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();
} 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) => {
navigator.clipboard.writeText(text);
};
@@ -168,7 +154,6 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
setUsername('');
setNewPrivateKey('');
setNewPublicKey('');
setNip55Url('');
setError(null);
setCreateStep('username');
};
@@ -197,6 +182,15 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
</div>
<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">
<button
onClick={() => setLoginMethod('extension')}
@@ -231,17 +225,6 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<Plus className="w-4 h-4 inline mr-1" />
Create
</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>
{loginMethod === 'extension' && (
@@ -249,12 +232,19 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<p className="text-gray-600 mb-4">
Connect using your Nostr browser extension (Alby, nos2x, etc.)
</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
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"
>
{loading ? 'Connecting...' : 'Connect Extension'}
{loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'Connect Extension'}
</button>
</div>
)}
@@ -263,29 +253,29 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Public or Private Key
Private Key (nsec)
</label>
<input
type="password"
value={keyInput}
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"
disabled={loading}
/>
{keyType !== 'unknown' && (
<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>
)}
</div>
<button
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"
>
{loading ? 'Connecting...' : 'Connect with Keys'}
{loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'Connect with Keys'}
</button>
</div>
)}
@@ -322,9 +312,9 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
{loginMethod === 'create' && createStep === 'keys' && (
<div className="space-y-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">
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>
</div>
@@ -342,6 +332,7 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button
onClick={() => copyToClipboard(newPrivateKey)}
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" />
</button>
@@ -362,6 +353,7 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button
onClick={() => copyToClipboard(newPublicKey)}
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" />
</button>
@@ -370,39 +362,10 @@ export const LoginModal: React.FC<LoginModalProps> = ({ isOpen, onClose }) => {
<button
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"
>
{loading ? 'Connecting...' : 'Use These Keys'}
</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'}
{loading ? 'Connecting...' : !isNdkReady ? 'Waiting for relays...' : 'I Saved My Keys - Continue'}
</button>
</div>
)}
+52 -65
View File
@@ -1,5 +1,5 @@
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 { getPublicKey, nip19 } from 'nostr-tools';
import { DEFAULT_RELAYS } from '../utils/nostr';
@@ -44,7 +44,7 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
useEffect(() => {
const initNDK = async () => {
try {
console.log('Initializing NDK with Dexie cache...');
console.log('Initializing NDK with relays:', DEFAULT_RELAYS);
// Initialize Dexie cache adapter
const dexieAdapter = new NDKCacheAdapterDexie({
@@ -56,11 +56,22 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
cacheAdapter: dexieAdapter,
});
console.log('Connecting to relays...');
await ndkInstance.connect();
console.log('NDK connected successfully with cache');
// Set NDK instance immediately so UI can render
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);
} catch (error) {
console.error('Failed to initialize NDK:', error);
@@ -72,56 +83,39 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
}, []);
const login = async () => {
if (!ndk) return;
if (!ndk) {
throw new Error('NDK not initialized. Please wait for connection to relays.');
}
try {
setIsLoading(true);
console.log('Attempting login with NIP-07 extension...');
// Check for NIP-07 extension
if (window.nostr) {
console.log('Nostr extension found, getting public key...');
const pubkey = await window.nostr.getPublicKey();
console.log('Got public key:', pubkey);
if (typeof window !== 'undefined' && window.nostr) {
console.log('Nostr extension found, creating NIP-07 signer...');
const ndkUser = ndk.getUser({ pubkey });
// Use NDK's built-in NIP-07 signer
const nip07Signer = new NDKNip07Signer();
ndk.signer = nip07Signer;
// Set signer
ndk.signer = {
user: async () => ndkUser,
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;
// Wait for the signer to be ready and get the user
const ndkUser = await nip07Signer.user();
console.log('Got user from signer:', ndkUser.pubkey);
setUser(ndkUser);
setIsConnected(true);
console.log('Login successful with NIP-07');
// Fetch user profile
const profile = await fetchUserProfile(pubkey);
setUserProfile(profile);
// Fetch user profile (don't fail if this fails)
try {
const profile = await fetchUserProfile(ndkUser.pubkey);
setUserProfile(profile);
} catch (profileError) {
console.warn('Could not fetch profile:', profileError);
}
} 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) {
console.error('Login failed:', error);
@@ -132,7 +126,9 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
};
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 {
setIsLoading(true);
@@ -191,9 +187,13 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
setIsConnected(true);
console.log('Login successful with keys');
// Fetch user profile
const profile = await fetchUserProfile(pubkey);
setUserProfile(profile);
// Fetch user profile (don't fail if this fails)
try {
const profile = await fetchUserProfile(pubkey);
setUserProfile(profile);
} catch (profileError) {
console.warn('Could not fetch profile:', profileError);
}
} catch (error) {
console.error('Keys login failed:', error);
throw error;
@@ -202,26 +202,13 @@ export const NDKProvider: React.FC<NDKProviderProps> = ({ children }) => {
}
};
const loginWithNip55 = async (url: string) => {
if (!ndk) return;
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);
const loginWithNip55 = async (_url: string) => {
if (!ndk) {
throw new Error('NDK not initialized. Please wait for connection to relays.');
}
// 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 = () => {
+11 -7
View File
@@ -169,13 +169,17 @@ export function getCounterFilter(pubkey?: string, isPublic?: boolean): NDKFilter
return filter;
}
export const DEFAULT_RELAYS = [
'wss://relay.azzamo.net',
'wss://relay.damus.io',
'wss://nostr.oxtr.dev',
'wss://nos.lol',
'wss://relay.snort.social',
];
// 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.damus.io',
'wss://nostr.oxtr.dev',
'wss://nos.lol',
'wss://relay.snort.social',
];
export function parseNip05(nip05: string): { name: string; domain: string } | 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;
}