Commit a320f5af authored by Your Name's avatar Your Name

Fix reverse proxy URL generation and bump version to 0.99.1

- Fix login redirect after authentication not respecting proxy subpaths
- Modified url_for function to return relative URLs when behind reverse proxy
- Updated login form action and template URLs to use url_for
- Fixed JavaScript fetch calls in providers and rotations templates
- Bumped version to 0.99.1 in all configuration files
- Updated CHANGELOG.md and PYPI.md with new version
parent 328cb8bf
......@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.99.1] - 2026-04-09
### Fixed
- **Reverse Proxy URL Generation**: Fixed login redirect after authentication not respecting proxy subpaths
- Modified `url_for` function to return relative URLs when behind reverse proxy (detected via `X-Forwarded-Prefix` header)
- Updated login form action to use `url_for` for consistency
- Fixed JavaScript fetch calls in providers and rotations templates to use proxy-aware URLs
- Login redirects now properly go to `domain/aisbf/dashboard` instead of `domain/dashboard`
### Changed
- **Version Bump**: Updated version to 0.99.1 in setup.py, pyproject.toml
## [0.99.0] - 2026-04-09
### Added
......
......@@ -41,8 +41,8 @@ python -m build
```
This creates:
- `dist/aisbf-0.9.0.tar.gz` - Source distribution
- `dist/aisbf-0.9.0-py3-none-any.whl` - Wheel distribution
- `dist/aisbf-0.99.1.tar.gz` - Source distribution
- `dist/aisbf-0.99.1-py3-none-any.whl` - Wheel distribution
## Testing the Package
......@@ -50,7 +50,7 @@ This creates:
```bash
# Install from the built wheel
pip install dist/aisbf-0.9.0-py3-none-any.whl
pip install dist/aisbf-0.99.1-py3-none-any.whl
# Test the installation
aisbf status
......
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.0"
__version__ = "0.99.1"
__all__ = [
# Config
"config",
......
......@@ -440,21 +440,27 @@ def get_base_url(request: Request) -> str:
def url_for(request: Request, path: str) -> str:
"""
Generate a proxy-aware URL for the given path.
Args:
request: The current request object
path: The path to generate URL for (should start with /)
Returns:
Full URL respecting proxy configuration
URL respecting proxy configuration - relative if behind proxy, full otherwise
"""
base_url = get_base_url(request)
root_path = request.scope.get("root_path", "")
# Ensure path starts with /
if not path.startswith("/"):
path = "/" + path
return f"{base_url}{path}"
if root_path:
# Behind proxy: return relative URL that browser resolves correctly
return root_path + path
else:
# Not behind proxy: return full URL
base_url = get_base_url(request)
return f"{base_url}{path}"
# Note: config will be imported after parsing CLI args if --config is provided
# For now, we'll delay the import and initialization
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.0"
version = "0.99.1"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.0",
version="0.99.1",
author="AISBF Contributors",
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",
......
......@@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
function restartServer() {
if (confirm('Are you sure you want to restart the server? This will disconnect all active connections.')) {
fetch('{{ url_for(request, "/dashboard/restart") }}', {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/restart") }}', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
......
......@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Date Range Filter Section -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Date Range</h3>
<form method="get" action="/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<form method="get" action="{{ url_for(request, "/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Time Range</label>
<select name="time_range" id="timeRangeSelect" style="width: 100%; padding: 10px; border-radius: 4px; background: #0f3460; color: white; border: 1px solid #2a4a7a;">
......@@ -78,7 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Filter by Provider/Model/Rotation/Autoselect -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Provider, Model, Rotation, Autoselect, or User</h3>
<form method="get" action="/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<form method="get" action="{{ url_for(request, "/dashboard/analytics" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: flex-end;">
<!-- Preserve date filter parameters -->
<input type="hidden" name="time_range" value="{{ selected_time_range }}">
{% if from_date %}<input type="hidden" name="from_date" value="{{ from_date }}">{% endif %}
......@@ -142,7 +142,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% if selected_provider or selected_model or selected_rotation or selected_autoselect or selected_user %}
<div>
<a href="/dashboard/analytics?time_range={{ selected_time_range }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}" style="padding: 10px 20px; background: #7f8c8d; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block;">
<a href="{{ url_for(request, "/dashboard/analytics?time_range={{ selected_time_range }}{% if from_date %}&from_date={{ from_date }}{% endif %}{% if to_date %}&to_date={{ to_date }}{% endif %}" style="padding: 10px 20px; background: #7f8c8d; color: white; border: none; border-radius: 4px; text-decoration: none; display: inline-block;">
Clear Filters
</a>
</div>
......
......@@ -62,12 +62,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<h3 style="margin-top: 30px; margin-bottom: 15px;">Quick Actions</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="/dashboard/providers" class="btn">Manage Providers</a>
<a href="/dashboard/rotations" class="btn">Manage Rotations</a>
<a href="/dashboard/autoselect" class="btn">Manage Autoselect</a>
<a href="/dashboard/prompts" class="btn">Manage Prompts</a>
<a href="/dashboard/rate-limits" class="btn">Rate Limits</a>
<a href="/dashboard/response-cache/stats" class="btn">Response Cache</a>
<a href="/dashboard/settings" class="btn btn-secondary">Server Settings</a>
<a href="{{ url_for(request, "/dashboard/providers" class="btn">Manage Providers</a>
<a href="{{ url_for(request, "/dashboard/rotations" class="btn">Manage Rotations</a>
<a href="{{ url_for(request, "/dashboard/autoselect" class="btn">Manage Autoselect</a>
<a href="{{ url_for(request, "/dashboard/prompts" class="btn">Manage Prompts</a>
<a href="{{ url_for(request, "/dashboard/rate-limits" class="btn">Rate Limits</a>
<a href="{{ url_for(request, "/dashboard/response-cache/stats" class="btn">Response Cache</a>
<a href="{{ url_for(request, "/dashboard/settings" class="btn btn-secondary">Server Settings</a>
</div>
{% endblock %}
......@@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="POST" action="/dashboard/login">
<form method="POST" action="{{ url_for(request, '/dashboard/login') }}">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
......
......@@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<button type="button" class="btn" id="add-provider-btn" onclick="showAddProviderForm()" style="margin-top: 20px;">Add Provider</button>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveProviders()">Save Configuration</button>
<a href="/dashboard" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
<script>
......@@ -912,7 +912,7 @@ async function authenticateCodex(key) {
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Codex OAuth2 Device Authorization flow...</p>';
try {
const response = await fetch('/dashboard/codex/auth/start', {
const response = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/codex/auth/start") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
......@@ -962,7 +962,7 @@ async function authenticateCodex(key) {
pollCount++;
try {
const pollResponse = await fetch('/dashboard/codex/auth/poll', {
const pollResponse = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/codex/auth/poll") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
......@@ -1015,7 +1015,7 @@ async function checkCodexAuth(key) {
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Checking Codex authentication status...</p>';
try {
const response = await fetch('/dashboard/codex/auth/status', {
const response = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/codex/auth/status") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
......@@ -1369,7 +1369,7 @@ async function authenticateClaude(key) {
This extension intercepts localhost OAuth2 callbacks and redirects them to your AISBF server.
</p>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<a href="/dashboard/extension/download" class="btn" style="background: #4a9eff; color: white; text-decoration: none; padding: 8px 15px; border-radius: 3px; font-size: 13px;">
<a href="{{ url_for(request, "/dashboard/extension/download" class="btn" style="background: #4a9eff; color: white; text-decoration: none; padding: 8px 15px; border-radius: 3px; font-size: 13px;">
📥 Download Extension
</a>
<button type="button" class="btn btn-secondary" onclick="showExtensionInstructions()" style="padding: 8px 15px; font-size: 13px;">
......@@ -1763,7 +1763,7 @@ function updateProviderCondenseMethod(providerKey, value) {
async function saveProviders() {
try {
const response = await fetch('/dashboard/providers', {
const response = await fetch('{{ url_for(request, "/dashboard/providers") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
......@@ -1772,7 +1772,7 @@ async function saveProviders() {
});
if (response.ok) {
window.location.href = '/dashboard/providers?success=1';
window.location.href = '{{ url_for(request, "/dashboard/providers?success=1") }}';
} else {
alert('Error saving configuration');
}
......
......@@ -43,7 +43,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<button type="button" class="btn" onclick="addRotation()" style="margin-top: 20px;">Add Rotation</button>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveRotations()">Save Configuration</button>
<a href="/dashboard" class="btn btn-secondary">Cancel</a>
<a href="{{ url_for(request, '/dashboard') }}" class="btn btn-secondary">Cancel</a>
</div>
<script>
......@@ -376,7 +376,7 @@ function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelInde
async function saveRotations() {
try {
const response = await fetch('/dashboard/rotations', {
const response = await fetch('{{ url_for(request, "/dashboard/rotations") }}', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
......@@ -385,7 +385,7 @@ async function saveRotations() {
});
if (response.ok) {
window.location.href = '/dashboard/rotations?success=1';
window.location.href = '{{ url_for(request, "/dashboard/rotations?success=1") }}';
} else {
alert('Error saving configuration');
}
......
......@@ -508,7 +508,7 @@ function createPersistentService() {
async function checkTorStatus() {
try {
const response = await fetch('{{ url_for(request, "/dashboard/tor/status") }}');
const response = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/tor/status") }}');
const status = await response.json();
const statusText = document.getElementById('tor-status-text');
......@@ -550,7 +550,7 @@ document.addEventListener('DOMContentLoaded', function() {
async function refreshCacheStats() {
try {
const response = await fetch('{{ url_for(request, "/dashboard/response-cache/stats") }}');
const response = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/response-cache/stats") }}');
const data = await response.json();
const statsText = document.getElementById('cache-stats-text');
......@@ -585,7 +585,7 @@ async function clearResponseCache() {
}
try {
const response = await fetch('{{ url_for(request, "/dashboard/response-cache/clear") }}', {
const response = await fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/response-cache/clear") }}', {
method: 'POST'
});
const data = await response.json();
......
......@@ -114,7 +114,7 @@ function closeModal() {
function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/autoselects") }}/' + encodeURIComponent(autoselectName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
......@@ -161,7 +161,7 @@ document.getElementById('autoselect-form').addEventListener('submit', function(e
formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/autoselects") }}';
const url = '{{ url_for(request, "{{ url_for(request, "/dashboard/user/autoselects") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
......
......@@ -156,7 +156,7 @@ function closeModal() {
function deleteProvider(providerName) {
if (!confirm(`Are you sure you want to delete the provider "${providerName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/providers") }}/' + encodeURIComponent(providerName), {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/providers") }}/' + encodeURIComponent(providerName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
......@@ -203,7 +203,7 @@ document.getElementById('provider-form').addEventListener('submit', function(e)
formData.append('provider_name', providerName);
formData.append('provider_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/providers") }}';
const url = '{{ url_for(request, "{{ url_for(request, "/dashboard/user/providers") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
......@@ -347,7 +347,7 @@ async function loadAuthFiles(providerId) {
<span style="color: #a0a0a0; font-size: 0.9em;"> (${formatFileSize(file.file_size)})</span>
</span>
<span>
<a href="/dashboard/user/providers/${encodeURIComponent(providerId)}/files/${file.file_type}/download" class="btn btn-secondary btn-sm" style="margin-right: 5px;">Download</a>
<a href="{{ url_for(request, "/dashboard/user/providers/${encodeURIComponent(providerId)}/files/${file.file_type}/download" class="btn btn-secondary btn-sm" style="margin-right: 5px;">Download</a>
<button class="btn btn-danger btn-sm" onclick="deleteAuthFile('${providerId}', '${file.file_type}')">Delete</button>
</span>
</div>
......
......@@ -114,7 +114,7 @@ function closeModal() {
function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return;
fetch('{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/rotations") }}/' + encodeURIComponent(rotationName), {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
......@@ -161,7 +161,7 @@ document.getElementById('rotation-form').addEventListener('submit', function(e)
formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj));
const url = '{{ url_for(request, "/dashboard/user/rotations") }}';
const url = '{{ url_for(request, "{{ url_for(request, "/dashboard/user/rotations") }}';
const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, {
......
......@@ -181,7 +181,7 @@ function closeModal() {
function deleteToken(tokenId) {
if (!confirm('Are you sure you want to delete this API token? This action cannot be undone and will immediately revoke access.')) return;
fetch('{{ url_for(request, "/dashboard/user/tokens") }}/' + tokenId, {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/tokens") }}/' + tokenId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
......@@ -228,7 +228,7 @@ document.getElementById('create-token-form').addEventListener('submit', function
formData.append('description', description);
}
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', {
fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST',
body: formData
}).then(response => {
......
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