00.99.48

parent 13b80367
...@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.99.47] - 2026-04-21 ## [0.99.48] - 2026-04-21
### Added ### Added
- **Unified Wallet System** - **Unified Wallet System**
...@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Clarified token access patterns (global vs user tokens) - Clarified token access patterns (global vs user tokens)
### Changed ### Changed
- Updated all version numbers to 0.99.47 - Updated all version numbers to 0.99.48
- Added wallet package to pyproject.toml - Added wallet package to pyproject.toml
## [0.99.26] - 2026-04-16 ## [0.99.26] - 2026-04-16
......
...@@ -41,8 +41,8 @@ python -m build ...@@ -41,8 +41,8 @@ python -m build
``` ```
This creates: This creates:
- `dist/aisbf-0.99.47.tar.gz` - Source distribution - `dist/aisbf-0.99.48.tar.gz` - Source distribution
- `dist/aisbf-0.99.47-py3-none-any.whl` - Wheel distribution - `dist/aisbf-0.99.48-py3-none-any.whl` - Wheel distribution
## Testing the Package ## Testing the Package
...@@ -50,7 +50,7 @@ This creates: ...@@ -50,7 +50,7 @@ This creates:
```bash ```bash
# Install from the built wheel # Install from the built wheel
pip install dist/aisbf-0.99.47-py3-none-any.whl pip install dist/aisbf-0.99.48-py3-none-any.whl
# Test the installation # Test the installation
aisbf status aisbf status
......
...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2 ...@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.47" __version__ = "0.99.48"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -4334,8 +4334,18 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta ...@@ -4334,8 +4334,18 @@ def DatabaseManager__run_config_migrations(self, cursor, auto_increment, timesta
logger.info("✅ Migration: Populated username, is_active and role for existing users") logger.info("✅ Migration: Populated username, is_active and role for existing users")
else: else:
# MySQL migrations would go here # MySQL: check and add any missing columns
pass for col_name, col_def, _ in required_columns:
try:
cursor.execute("""
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = %s
""", (col_name,))
if not cursor.fetchone():
cursor.execute(f'ALTER TABLE users ADD COLUMN {col_name} {col_def}')
logger.info(f"✅ Migration: Added {col_name} column to users")
except Exception as col_e:
logger.warning(f"Migration check for users.{col_name}: {col_e}")
except Exception as e: except Exception as e:
logger.warning(f"Migration check for users table columns: {e}") logger.warning(f"Migration check for users table columns: {e}")
......
...@@ -355,22 +355,36 @@ class PayPalPaymentHandler: ...@@ -355,22 +355,36 @@ class PayPalPaymentHandler:
return {'status': 'error', 'message': str(e)} return {'status': 'error', 'message': str(e)}
async def _verify_webhook_signature(self, payload: dict, headers: dict) -> bool: async def _verify_webhook_signature(self, payload: dict, headers: dict) -> bool:
""" """Verify PayPal webhook signature via PayPal's verify-webhook-signature API."""
Verify PayPal webhook signature webhook_id = self.webhook_secret # stored as 'webhook_secret' in admin settings
if not webhook_id:
For production, implement proper signature verification: logger.warning("PayPal webhook_id not configured - skipping signature verification")
https://developer.paypal.com/api/rest/webhooks/rest/#verify-webhook-signature
"""
# For now, basic verification - in production, verify the signature properly
if not self.webhook_secret:
logger.warning("PayPal webhook_secret not configured - skipping signature verification")
return True return True
# TODO: Implement proper webhook signature verification try:
# This requires calling PayPal's verify-webhook-signature endpoint access_token = await self.get_access_token()
# with the webhook_id, transmission_id, transmission_sig, etc. body = {
"auth_algo": headers.get("paypal-auth-algo", ""),
return True "cert_url": headers.get("paypal-cert-url", ""),
"transmission_id": headers.get("paypal-transmission-id", ""),
"transmission_sig": headers.get("paypal-transmission-sig", ""),
"transmission_time": headers.get("paypal-transmission-time", ""),
"webhook_id": webhook_id,
"webhook_event": payload,
}
response = await self.http_client.post(
f"{self.base_url}/v1/notifications/verify-webhook-signature",
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
json=body,
)
result = response.json()
verified = result.get("verification_status") == "SUCCESS"
if not verified:
logger.warning(f"PayPal webhook verification failed: {result}")
return verified
except Exception as e:
logger.error(f"PayPal webhook signature verification error: {e}")
return False
async def _handle_order_completed(self, resource: dict): async def _handle_order_completed(self, resource: dict):
"""Handle completed order (Vault v3)""" """Handle completed order (Vault v3)"""
......
...@@ -1363,6 +1363,7 @@ async def startup_event(): ...@@ -1363,6 +1363,7 @@ async def startup_event():
payment_service = PaymentService(db_manager, payment_config) payment_service = PaymentService(db_manager, payment_config)
await payment_service.initialize() await payment_service.initialize()
app.state.payment_service = payment_service
logger.info("Payment service started") logger.info("Payment service started")
except Exception as e: except Exception as e:
...@@ -3976,7 +3977,9 @@ async def dashboard_index(request: Request): ...@@ -3976,7 +3977,9 @@ async def dashboard_index(request: Request):
if request.session.get('role') == 'admin': if request.session.get('role') == 'admin':
# Admin dashboard # Admin dashboard
return templates.TemplateResponse( db = DatabaseRegistry.get_config_database()
users_count = len(db.get_users())
return templates.TemplateResponse(
request=request, request=request,
name="dashboard/index.html", name="dashboard/index.html",
context={ context={
...@@ -3986,7 +3989,8 @@ async def dashboard_index(request: Request): ...@@ -3986,7 +3989,8 @@ async def dashboard_index(request: Request):
"providers_count": len(config.providers) if config else 0, "providers_count": len(config.providers) if config else 0,
"rotations_count": len(config.rotations) if config else 0, "rotations_count": len(config.rotations) if config else 0,
"autoselect_count": len(config.autoselect) if config else 0, "autoselect_count": len(config.autoselect) if config else 0,
"server_config": server_config or {} "server_config": server_config or {},
"users_count": users_count,
} }
) )
else: else:
......
...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" ...@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "aisbf" name = "aisbf"
version = "0.99.47" version = "0.99.48"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations" description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
......
...@@ -49,7 +49,7 @@ class InstallCommand(_install): ...@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup( setup(
name="aisbf", name="aisbf",
version="0.99.47", version="0.99.48",
author="AISBF Contributors", author="AISBF Contributors",
author_email="stefy@nexlab.net", author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations", description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
...@@ -283,13 +283,27 @@ document.addEventListener('DOMContentLoaded', function () { ...@@ -283,13 +283,27 @@ document.addEventListener('DOMContentLoaded', function () {
}) })
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data.type === 'stripe' && data.checkout_url) { this.disabled = false;
window.location.href = data.checkout_url; this.innerHTML = orig;
} else if (data.type === 'paypal' && data.approval_url) { let url = null;
window.location.href = data.approval_url; if (data.type === 'stripe' && data.checkout_url) url = data.checkout_url;
else if (data.type === 'paypal' && data.approval_url) url = data.approval_url;
if (url) {
const popup = window.open(url, 'payment_popup', 'width=600,height=700,scrollbars=yes,resizable=yes');
if (!popup) {
// Blocked by browser — open in new tab
window.open(url, '_blank');
return;
}
// Poll for popup close then reload wallet page
const timer = setInterval(() => {
if (popup.closed) {
clearInterval(timer);
window.location.reload();
}
}, 800);
} else { } else {
this.disabled = false;
this.innerHTML = orig;
showAlert(data.error || 'Failed to initiate checkout.', 'Error', '❌', 'danger'); showAlert(data.error || 'Failed to initiate checkout.', 'Error', '❌', 'danger');
} }
}) })
...@@ -435,7 +449,8 @@ function openCryptoModal(name) { ...@@ -435,7 +449,8 @@ function openCryptoModal(name) {
document.getElementById('cryptoModal').classList.add('active'); document.getElementById('cryptoModal').classList.add('active');
// Fetch a per-user deposit address from the server // Fetch a per-user deposit address from the server
const amount = getAmount() || 15; const customVal = parseFloat(document.getElementById('custom-amount').value);
const amount = customVal || selectedAmount || 15;
fetch('/dashboard/wallet/topup', { fetch('/dashboard/wallet/topup', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
......
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