Files
nostr-oidc-bridge/views/login.ejs
2025-08-18 13:59:12 -04:00

232 lines
9.6 KiB
Plaintext

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OIDC Bridge v2.4</title>
<style>
body {
background: black
}
</style>
</head>
<body>
<div id="login-container">
<!-- Hidden data for backend processing -->
<div id="challenge" style="display: none;"><%= challenge %></div>
<div id="session-id" style="display: none;"><%= uid %></div>
<div id="return-to" style="display: none;"><%= returnTo %></div>
<!-- Error display if needed -->
<% if (typeof error !== 'undefined') { %>
<div id="error-message" style="color: #dc3545; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; margin: 20px; text-align: center;">
<strong>Authentication Error:</strong> <%= error %>
</div>
<% } %>
<!-- Target div for nostr-login widget -->
<div id="nostr-login"></div>
<!-- Version info -->
</div>
<!-- Load your hosted nostr-login with custom laan theme configuration -->
<script src='https://laantungir.net/nostr-login/unpkg.js'
data-theme='laan'
data-start-screen='welcome'
data-no-banner='true'></script>
<!-- Import nostr-tools for proper npub encoding -->
<script type="module">
import { nip19 } from 'https://esm.sh/nostr-tools@2.16.2';
window.nip19 = nip19;
</script>
<!-- Integration bridge to handle authentication -->
<script>
let isProcessing = false;
// Widget initialization is now handled automatically by the script data attributes
console.log('Nostr-login widget will initialize automatically with laan theme and immediate login display');
// Auto-launch the login modal immediately when page loads
window.addEventListener('load', () => {
// Small delay to ensure nostr-login widget is fully initialized
setTimeout(() => {
console.log('Auto-launching nostr-login modal...');
document.dispatchEvent(new CustomEvent('nlLaunch', { detail: 'login' }));
}, 100);
});
// Listen for nostr-login authentication events
document.addEventListener('nlAuth', async (e) => {
console.log('nostr-login auth event:', e.detail);
if ((e.detail.type === 'login' || e.detail.type === 'signup') && !isProcessing) {
isProcessing = true;
await handleNostrLogin();
}
});
async function handleNostrLogin() {
try {
console.log('Handling Nostr login...');
// Get the challenge and session info
const challenge = document.getElementById('challenge').textContent.trim();
const sessionId = document.getElementById('session-id').textContent.trim();
const returnTo = document.getElementById('return-to').textContent.trim();
console.log('Challenge:', challenge);
console.log('Session ID:', sessionId);
if (!window.nostr) {
throw new Error('window.nostr not available after login');
}
// Get the user's public key
const pubkey = await window.nostr.getPublicKey();
console.log('Got pubkey:', pubkey);
// Properly encode pubkey to npub format using nip19
let npub;
try {
if (window.nip19) {
npub = window.nip19.npubEncode(pubkey);
console.log('Encoded npub:', npub);
} else {
console.warn('nip19 not available, using raw pubkey');
npub = pubkey; // Fallback to raw hex pubkey
}
} catch (error) {
console.error('Error encoding npub:', error);
npub = pubkey; // Fallback to raw hex pubkey
}
// Create and sign the challenge event
const eventTemplate = {
kind: 1,
content: challenge,
created_at: Math.floor(Date.now() / 1000),
tags: []
};
console.log('Signing event template:', eventTemplate);
const signedEvent = await window.nostr.signEvent(eventTemplate);
console.log('Signed event:', signedEvent);
// Verify the signature locally before sending
if (signedEvent.pubkey !== pubkey) {
throw new Error('Signed event pubkey mismatch');
}
if (signedEvent.content !== challenge) {
throw new Error('Signed event content mismatch');
}
// Submit to backend
await submitAuthToBackend(sessionId, pubkey, signedEvent, npub);
} catch (error) {
console.error('Authentication error:', error);
isProcessing = false;
// Show user-friendly error
showError('Authentication failed: ' + error.message);
}
}
async function submitAuthToBackend(sessionId, pubkey, signedEvent, npub) {
try {
console.log('✅ Submitting auth to backend...');
console.log('📋 Form data:', {
sessionId,
npub: npub ? `${npub.substring(0, 20)}... (length: ${npub.length})` : 'undefined',
pubkey: pubkey ? `${pubkey.substring(0, 20)}... (length: ${pubkey.length})` : 'undefined',
eventContent: signedEvent.content.substring(0, 20) + '...'
});
// Validate inputs before submission
if (!npub) {
throw new Error('npub is required but not provided');
}
if (!signedEvent || !signedEvent.sig) {
throw new Error('Valid signed event is required');
}
// Create a form and submit it traditionally to allow proper redirects
// This avoids CORS issues when redirecting to external domains like Gitea
const form = document.createElement('form');
form.method = 'POST';
form.action = `/complete-auth/${sessionId}`;
// Add form fields
const npubField = document.createElement('input');
npubField.type = 'hidden';
npubField.name = 'npub';
npubField.value = npub;
form.appendChild(npubField);
const eventField = document.createElement('input');
eventField.type = 'hidden';
eventField.name = 'event_json';
eventField.value = JSON.stringify(signedEvent);
form.appendChild(eventField);
// Add form to document and submit
document.body.appendChild(form);
console.log('🚀 Submitting form for authentication...');
// Add timeout to prevent infinite waiting
setTimeout(() => {
if (isProcessing) {
console.warn('⚠️ Form submission timeout - authentication may have failed');
showError('Authentication timed out. Please try again.');
isProcessing = false;
document.body.removeChild(form);
}
}, 30000); // 30 second timeout
form.submit();
} catch (error) {
console.error('❌ Backend submission error:', error);
throw error;
}
}
function showError(message) {
// Create or update error display
let errorDiv = document.getElementById('error-message');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.id = 'error-message';
errorDiv.style.cssText = 'color: #dc3545; background-color: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 8px; margin: 20px; text-align: center;';
document.getElementById('login-container').insertBefore(errorDiv, document.getElementById('login-container').firstChild);
}
errorDiv.innerHTML = `<strong>Authentication Error:</strong> ${message}`;
// Reset processing flag after showing error
setTimeout(() => {
isProcessing = false;
}, 2000);
}
// Handle any initialization errors
window.addEventListener('error', (e) => {
console.error('Global error:', e.error);
if (e.error && e.error.message.includes('nostr')) {
showError('Nostr login initialization failed. Please refresh the page.');
}
});
console.log('Nostr OIDC Bridge login page initialized');
</script>
</body>
</html>