0.99.51

parent 8c425f1d
......@@ -96,12 +96,12 @@ curl -X POST http://localhost:17765/api/wallet/topup \
## Documentation
For complete documentation, configuration guides, and API reference:
- **[📚 Full Documentation](DOCUMENTATION.md)** - Comprehensive user and developer guide
- **[🔧 Installation Guide](DOCUMENTATION.md#installation)** - Detailed setup instructions
- **[⚙️ Configuration](DOCUMENTATION.md#configuration)** - All configuration options
- **[💰 Wallet System](DOCUMENTATION.md#wallet-system)** - Complete wallet documentation
- **[🔌 API Reference](DOCUMENTATION.md#api-endpoints)** - Complete API documentation
- **[🛠️ Development](DOCUMENTATION.md#development)** - Development and deployment guides
- **[📚 Full Documentation](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md)** - Comprehensive user and developer guide
- **[🔧 Installation Guide](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md#installation)** - Detailed setup instructions
- **[⚙️ Configuration](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md#configuration)** - All configuration options
- **[💰 Wallet System](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md#wallet-system)** - Complete wallet documentation
- **[🔌 API Reference](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md#api-endpoints)** - Complete API documentation
- **[🛠️ Development](https://git.nexlab.net/nexlab/aisbf/src/master/DOCUMENTATION.md#development)** - Development and deployment guides
## 🚀 Support AISBF - Your Donations Matter!
......
......@@ -200,6 +200,45 @@ class StripePaymentHandler:
logger.error(f"Error creating Stripe top up intent: {e}")
return {'success': False, 'error': str(e)}
async def create_topup_checkout_session(self, user_id: int, amount: Decimal, success_url: str, cancel_url: str) -> dict:
"""Create a Stripe Checkout Session for wallet top-up (hosted redirect flow)."""
try:
customer_id = await self._get_or_create_customer(user_id)
currency = self.config.get('currency_code', 'usd').lower()
session = await asyncio.to_thread(
stripe.checkout.Session.create,
customer=customer_id,
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': currency,
'product_data': {'name': 'Wallet Top-Up'},
'unit_amount': int(amount * 100),
},
'quantity': 1,
}],
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
metadata={
'user_id': str(user_id),
'topup': 'true',
'amount': str(amount),
},
payment_intent_data={
'metadata': {
'user_id': str(user_id),
'topup': 'true',
'amount': str(amount),
},
},
)
logger.info(f"Created Stripe checkout session for user {user_id}: {session.id}")
return {'success': True, 'checkout_url': session.url, 'session_id': session.id}
except Exception as e:
logger.error(f"Error creating Stripe checkout session: {e}")
return {'success': False, 'error': str(e)}
async def _handle_payment_succeeded(self, payment_intent: dict):
"""Handle successful Stripe payment — credits user wallet for top-up intents."""
logger.info(f"Payment succeeded: {payment_intent['id']}")
......
This diff is collapsed.
......@@ -523,7 +523,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if request.session.user_id %}
<div class="account-menu">
<div class="account-trigger" onclick="toggleAccountMenu()">
<img src="{{ request.session.profile_pic if request.session.profile_pic else 'https://www.gravatar.com/avatar/' ~ (request.session.email|md5 if request.session.email else '') ~ '?s=48&d=identicon' }}" alt="User avatar">
<img src="{{ '/dashboard/profile-pic' if request.session.has_profile_pic else 'https://www.gravatar.com/avatar/' ~ (request.session.email|md5 if request.session.email else '') ~ '?s=48&d=identicon' }}" alt="User avatar">
<span>Account</span>
</div>
<div class="account-dropdown" id="account-dropdown">
......
......@@ -89,21 +89,34 @@
<div style="margin-bottom: 20px;">
<label style="display: block; margin-bottom: 10px; color: #e0e0e0; font-weight: 500;">
Set New Encryption Key (44 characters, base64)
Set New Encryption Key (44 characters, URL-safe base64)
</label>
<input type="text" id="encryptionKey" placeholder="Leave empty to auto-generate" style="width: 100%; padding: 10px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-family: monospace;">
<input type="text" id="encryptionKey" placeholder="Click Generate to create a secure key" style="width: 100%; padding: 10px; border: 1px solid #0f3460; border-radius: 5px; background: #1a1a2e; color: #e0e0e0; font-family: monospace;">
<div style="margin-top: 10px; color: #888; font-size: 14px;">
<i class="fas fa-info-circle me-2"></i>Leave empty to auto-generate a secure key. Copy and save it securely - you cannot retrieve it later.
<i class="fas fa-info-circle me-2"></i>Generate a key, copy it to a safe place, then save it. You cannot retrieve it from the interface later.
</div>
</div>
<div style="display: flex; gap: 10px;">
<div id="encryptionKeyBackup" style="display:none; margin-bottom: 20px; padding: 15px; background: #2d1b00; border: 2px solid #f39c12; border-radius: 8px;">
<div style="color: #f39c12; font-weight: bold; margin-bottom: 8px;">
<i class="fas fa-exclamation-triangle me-2"></i>Save this key now — it will not be shown again after you leave this page
</div>
<div style="font-family: monospace; font-size: 14px; color: #e0e0e0; word-break: break-all;" id="encryptionKeyBackupValue"></div>
<button type="button" class="btn btn-sm mt-2" onclick="copyEncryptionKey()" style="background: #f39c12; color: #000;">
<i class="fas fa-copy me-1"></i>Copy to Clipboard
</button>
</div>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button type="button" class="btn" onclick="generateEncryptionKey()" style="background: #9b59b6; color: white;">
<i class="fas fa-random me-2"></i>Generate Random Key
<i class="fas fa-random me-2"></i>Generate New Key
</button>
<button type="button" class="btn" onclick="saveEncryptionKey()" style="background: #e74c3c; color: white;">
<i class="fas fa-save me-2"></i>Save Encryption Key
</button>
<button type="button" class="btn" onclick="resetCryptoSeeds()" style="background: #c0392b; color: white;">
<i class="fas fa-trash me-2"></i>Reset Crypto Seeds
</button>
</div>
</div>
......@@ -657,46 +670,60 @@ async function loadEncryptionKeyStatus() {
}
}
// Generate random encryption key
// Generate random encryption key (URL-safe base64, required by Fernet)
function generateEncryptionKey() {
// Generate 32 random bytes and encode as base64 (44 characters)
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const base64 = btoa(String.fromCharCode.apply(null, array));
// URL-safe base64: replace + with - and / with _
const base64 = btoa(String.fromCharCode.apply(null, array))
.replace(/\+/g, '-').replace(/\//g, '_');
document.getElementById('encryptionKey').value = base64;
showToast('Random encryption key generated. Save it securely!', 'warning');
document.getElementById('encryptionKeyBackup').style.display = 'block';
document.getElementById('encryptionKeyBackupValue').textContent = base64;
showToast('Encryption key generated. Copy it now and store it safely before saving!', 'warning');
}
// Copy key to clipboard then hide it
function copyEncryptionKey() {
const key = document.getElementById('encryptionKey').value.trim();
if (!key) return;
navigator.clipboard.writeText(key).then(() => {
showToast('Key copied to clipboard. Store it safely!', 'success');
document.getElementById('encryptionKeyBackup').style.display = 'none';
document.getElementById('encryptionKey').value = '';
});
}
// Save encryption key
async function saveEncryptionKey() {
const key = document.getElementById('encryptionKey').value.trim();
if (!key) {
showToast('Please enter or generate an encryption key', 'danger');
return;
}
if (key.length !== 44) {
showToast('Encryption key must be 44 characters (base64 encoded)', 'danger');
showToast('Encryption key must be 44 characters (URL-safe base64)', 'danger');
return;
}
const ok = await showDangerConfirm('WARNING: Setting or changing the encryption key will affect all encrypted data. Are you sure?', 'Change Encryption Key');
if (!ok) return;
try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/encryption-key") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({encryption_key: key})
});
const result = await response.json();
if (response.ok && result.success) {
showToast('Encryption key saved successfully. Server restart required to take effect.', 'success');
document.getElementById('encryptionKey').value = '';
showToast('Encryption key saved. COPY THE KEY ABOVE before restarting the server!', 'success');
loadEncryptionKeyStatus();
// Do NOT clear the field — user needs to copy it for backup
} else {
showToast(result.error || 'Failed to save encryption key', 'danger');
}
......@@ -705,6 +732,30 @@ async function saveEncryptionKey() {
}
}
// Reset crypto wallet seeds (delete master keys so they are regenerated with the new encryption key)
async function resetCryptoSeeds() {
const ok = await showDangerConfirm(
'WARNING: This will delete all crypto master seeds. All existing derived addresses will be lost. Only do this after setting a new encryption key. Are you sure?',
'Reset Crypto Seeds'
);
if (!ok) return;
try {
const response = await fetch('{{ url_for(request, "/api/admin/settings/crypto-seeds-reset") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
});
const result = await response.json();
if (response.ok && result.success) {
showToast('Crypto seeds deleted. Restart the server to generate new ones.', 'success');
} else {
showToast(result.error || 'Failed to reset crypto seeds', 'danger');
}
} catch (error) {
showToast('Error: ' + error.message, 'danger');
}
}
// Load system status
async function loadSystemStatus() {
try {
......
......@@ -101,79 +101,70 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<script>
// OAuth2 Popup Handling
function openOAuthPopup(url) {
// Add popup parameter to URL
const separator = url.includes('?') ? '&' : '?';
const popupUrl = url + separator + 'popup=1';
// Calculate popup position (center screen)
const width = 500;
const height = 650;
const width = 500, height = 650;
const left = (window.innerWidth / 2) - (width / 2);
const top = (window.innerHeight / 2) - (height / 2);
// Open popup window
const popup = window.open(
popupUrl,
'OAuth2 Authentication',
`width=${width},height=${height},top=${top},left=${left},resizable=no,scrollbars=yes,status=no`
);
// Listen for popup messages
let done = false;
function onSuccess() {
if (done) return;
done = true;
cleanup();
window.location.replace('/dashboard');
}
function onError(msg) {
if (done) return;
done = true;
cleanup();
showAlert(msg || 'Authentication failed', 'Authentication Error', '❌', 'danger');
}
// BroadcastChannel: works even when COOP severs window.opener
let bc = null;
try {
bc = new BroadcastChannel('oauth2_result');
bc.onmessage = function(e) {
if (e.data && e.data.type === 'oauth2_complete') onSuccess();
else if (e.data && e.data.type === 'oauth2_error') onError(e.data.error);
};
} catch(e) {}
// postMessage fallback (same-origin popups where opener is available)
const messageListener = function(event) {
console.log('Received OAuth message:', event.data, event.origin);
if (event.data && event.data.type === 'oauth2_complete') {
console.log('OAuth complete, redirecting to:', event.data.redirect_url || '/dashboard');
// Close popup
popup.close();
// Remove listener
window.removeEventListener('message', messageListener);
// Force redirect to dashboard - use relative path for reliability
window.location.replace('/dashboard');
} else if (event.data && event.data.type === 'oauth2_error') {
console.log('OAuth error:', event.data.error);
// Close popup
popup.close();
// Remove listener
window.removeEventListener('message', messageListener);
// Show error
showAlert(event.data.error || 'Authentication failed', 'Authentication Error', '❌', 'danger');
}
if (!event.data) return;
if (event.data.type === 'oauth2_complete') onSuccess();
else if (event.data.type === 'oauth2_error') onError(event.data.error);
};
window.addEventListener('message', messageListener);
// Fallback if popup is blocked or closed manually
// Popup-closed fallback: poll auth status via API (not just any 200)
const popupCheck = setInterval(function() {
if (popup.closed) {
clearInterval(popupCheck);
window.removeEventListener('message', messageListener);
// Check if authentication was successful by trying to load dashboard
// Use fetch with credentials: 'same-origin' to send cookies
fetch('/dashboard', {
method: 'GET',
credentials: 'same-origin',
redirect: 'follow'
}).then(response => {
// If we get a 200 OK (HTML) or redirect to dashboard, auth was successful
if (response.ok || response.redirected) {
console.log('Auth detected on popup close, redirecting to dashboard');
window.location.replace('/dashboard');
}
}).catch(error => {
console.log('Dashboard check failed:', error);
// Fallback: try direct redirect anyway for authenticated users
window.location.replace('/dashboard');
});
}
}, 500);
let closed = false;
try { closed = popup.closed; } catch(e) { /* COOP blocks access */ }
if (!closed) return;
clearInterval(popupCheck);
if (done) return;
fetch('/dashboard/api/auth-check', { credentials: 'same-origin' })
.then(r => r.json())
.then(data => { if (data.authenticated) onSuccess(); })
.catch(() => {});
}, 600);
function cleanup() {
clearInterval(popupCheck);
window.removeEventListener('message', messageListener);
if (bc) { bc.close(); bc = null; }
try { popup.close(); } catch(e) {}
}
}
</script>
</div>
......
......@@ -51,7 +51,7 @@
<div style="display: flex; align-items: flex-start; gap: 1.5rem; flex-wrap: wrap;">
<div style="position: relative; cursor: pointer; flex-shrink: 0;" id="avatar-wrap">
<img id="avatar-preview"
src="{{ user.profile_pic if user.profile_pic else 'https://www.gravatar.com/avatar/' ~ (session.email|md5 if session.email else '') ~ '?s=96&d=identicon' }}"
src="{{ '/dashboard/profile-pic' if user.profile_pic else 'https://www.gravatar.com/avatar/' ~ (session.email|md5 if session.email else '') ~ '?s=96&d=identicon' }}"
alt="Profile picture"
style="width: 96px; height: 96px; border-radius: 8px; object-fit: cover; display: block;">
<div id="avatar-overlay" style="position: absolute; inset: 0; background: rgba(0,0,0,0.45); border-radius: 8px; display: flex; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s; pointer-events: none;">
......
......@@ -289,7 +289,7 @@
<!-- Hero -->
<div class="hero">
<div class="hero-avatar">
<img src="{{ session.profile_pic if session.profile_pic else 'https://www.gravatar.com/avatar/' ~ (session.email|md5 if session.email else '') ~ '?s=128&d=identicon' }}" alt="avatar">
<img src="{{ '/dashboard/profile-pic' if session.has_profile_pic else 'https://www.gravatar.com/avatar/' ~ (session.email|md5 if session.email else '') ~ '?s=128&d=identicon' }}" alt="avatar">
</div>
<div class="hero-text">
<h2>Welcome back, {{ display_name }}!</h2>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment