enki b3204ea07a
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
first commit
2025-08-18 00:40:15 -07:00

364 lines
12 KiB
JavaScript

// 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();