// Nostr Authentication Module class NostrAuth { constructor() { this.sessionToken = localStorage.getItem('session_token'); this.pubkey = localStorage.getItem('user_pubkey'); } // Check if user is authenticated isAuthenticated() { return !!this.sessionToken && !!this.pubkey; } // Get current user pubkey getCurrentUser() { return this.pubkey; } // NIP-07 Login via browser extension (Alby, nos2x, etc.) async loginNIP07() { try { if (!window.nostr) { return { success: false, message: 'No Nostr extension detected. Please install a Nostr browser extension like Alby or nos2x, then refresh the page.' }; } // Get challenge from server const challengeResponse = await fetch('/api/auth/challenge'); if (!challengeResponse.ok) { if (challengeResponse.status === 429) { return { success: false, message: 'Too many login attempts. Please wait a moment and try again.' }; } return { success: false, message: 'Server unavailable. Please try again later.' }; } const challengeData = await challengeResponse.json(); if (!challengeData.challenge) { return { success: false, message: 'Invalid server response. Please try again.' }; } // Get pubkey from extension let pubkey; try { pubkey = await window.nostr.getPublicKey(); } catch (error) { return { success: false, message: 'Extension denied access or is locked. Please unlock your Nostr extension and try again.' }; } if (!pubkey || pubkey.length !== 64) { return { success: false, message: 'Invalid public key from extension. Please check your Nostr extension setup.' }; } // Create authentication event (using kind 27235 for HTTP auth per NIP-98) const authEvent = { kind: 27235, created_at: Math.floor(Date.now() / 1000), tags: [ ['u', window.location.origin + '/api/auth/login'], ['method', 'POST'], ['challenge', challengeData.challenge] ], content: '', pubkey: pubkey }; // Sign the event let signedEvent; try { signedEvent = await window.nostr.signEvent(authEvent); } catch (error) { return { success: false, message: 'Signing was cancelled or failed. Please try again and approve the signature request.' }; } if (!signedEvent || !signedEvent.sig) { return { success: false, message: 'Invalid signature from extension. Please try again.' }; } // Send to server for validation const loginResponse = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ auth_type: 'nip07', auth_event: JSON.stringify({ event: signedEvent, challenge: challengeData.challenge }) }) }); const loginData = await loginResponse.json(); if (!loginResponse.ok) { if (loginResponse.status === 429) { return { success: false, message: 'Too many login attempts. Please wait a minute and try again.' }; } else if (loginResponse.status === 401) { return { success: false, message: 'Authentication failed. Please check your Nostr extension and try again.' }; } else if (loginResponse.status >= 500) { return { success: false, message: 'Server error. Please try again later.' }; } return { success: false, message: loginData.message || 'Login failed. Please try again.' }; } // Store session info this.sessionToken = loginData.session_token; this.pubkey = loginData.pubkey; localStorage.setItem('session_token', this.sessionToken); localStorage.setItem('user_pubkey', this.pubkey); return { success: true, pubkey: this.pubkey, message: 'Successfully logged in via NIP-07' }; } catch (error) { console.error('NIP-07 login failed:', error); return { success: false, message: error.message }; } } // NIP-46 Login via bunker URL async loginNIP46(bunkerURL) { try { if (!bunkerURL || (!bunkerURL.startsWith('bunker://') && !bunkerURL.startsWith('nostrconnect://'))) { throw new Error('Invalid bunker URL format. Expected: bunker://... or nostrconnect://...'); } // Send bunker URL to server for validation const loginResponse = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ auth_type: 'nip46', bunker_url: bunkerURL }) }); const loginData = await loginResponse.json(); if (!loginResponse.ok) { throw new Error(loginData.message || 'NIP-46 login failed'); } // Store session info this.sessionToken = loginData.session_token; this.pubkey = loginData.pubkey; localStorage.setItem('session_token', this.sessionToken); localStorage.setItem('user_pubkey', this.pubkey); return { success: true, pubkey: this.pubkey, message: 'Successfully logged in via NIP-46' }; } catch (error) { console.error('NIP-46 login failed:', error); return { success: false, message: error.message }; } } // Logout async logout() { try { await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); } catch (error) { console.error('Logout request failed:', error); } // Clear local storage this.sessionToken = null; this.pubkey = null; localStorage.removeItem('session_token'); localStorage.removeItem('user_pubkey'); } // Get user statistics async getUserStats() { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } const response = await fetch('/api/users/me/stats', { credentials: 'include', headers: { 'Authorization': `Bearer ${this.sessionToken}` } }); if (!response.ok) { if (response.status === 401 || response.status === 403) { // Clear invalid session data this.sessionToken = null; this.pubkey = null; localStorage.removeItem('session_token'); localStorage.removeItem('user_pubkey'); throw new Error(`${response.status} Unauthorized - session expired`); } throw new Error(`Failed to get user stats (${response.status})`); } return await response.json(); } // Get user's files async getUserFiles() { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } const response = await fetch('/api/users/me/files', { credentials: 'include', headers: { 'Authorization': `Bearer ${this.sessionToken}` } }); if (!response.ok) { if (response.status === 401 || response.status === 403) { // Clear invalid session data this.sessionToken = null; this.pubkey = null; localStorage.removeItem('session_token'); localStorage.removeItem('user_pubkey'); throw new Error(`${response.status} Unauthorized - session expired`); } throw new Error(`Failed to get user files (${response.status})`); } return await response.json(); } // Delete a file async deleteFile(hash) { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } const response = await fetch(`/api/users/me/files/${hash}`, { method: 'DELETE', credentials: 'include', headers: { 'Authorization': `Bearer ${this.sessionToken}` } }); if (!response.ok) { if (response.status === 401 || response.status === 403) { // Clear invalid session data this.sessionToken = null; this.pubkey = null; localStorage.removeItem('session_token'); localStorage.removeItem('user_pubkey'); throw new Error(`${response.status} Unauthorized - session expired`); } const errorData = await response.text(); throw new Error(errorData || `Failed to delete file (${response.status})`); } return await response.json(); } // Update file access level async updateFileAccess(hash, accessLevel) { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } const response = await fetch(`/api/users/me/files/${hash}/access`, { method: 'PUT', credentials: 'include', headers: { 'Authorization': `Bearer ${this.sessionToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ access_level: accessLevel }) }); if (!response.ok) { if (response.status === 401 || response.status === 403) { // Clear invalid session data this.sessionToken = null; this.pubkey = null; localStorage.removeItem('session_token'); localStorage.removeItem('user_pubkey'); throw new Error(`${response.status} Unauthorized - session expired`); } const errorData = await response.text(); throw new Error(errorData || `Failed to update file access (${response.status})`); } return await response.json(); } // Make authenticated request async makeAuthenticatedRequest(url, options = {}) { if (!this.isAuthenticated()) { throw new Error('Not authenticated'); } const authOptions = { ...options, credentials: 'include', headers: { ...options.headers, 'Authorization': `Bearer ${this.sessionToken}` } }; return fetch(url, authOptions); } } // Global instance window.nostrAuth = new NostrAuth();