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 ...@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.99.0] - 2026-04-09
### Added ### Added
......
...@@ -41,8 +41,8 @@ python -m build ...@@ -41,8 +41,8 @@ python -m build
``` ```
This creates: This creates:
- `dist/aisbf-0.9.0.tar.gz` - Source distribution - `dist/aisbf-0.99.1.tar.gz` - Source distribution
- `dist/aisbf-0.9.0-py3-none-any.whl` - Wheel distribution - `dist/aisbf-0.99.1-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.9.0-py3-none-any.whl pip install dist/aisbf-0.99.1-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.0" __version__ = "0.99.1"
__all__ = [ __all__ = [
# Config # Config
"config", "config",
......
...@@ -440,21 +440,27 @@ def get_base_url(request: Request) -> str: ...@@ -440,21 +440,27 @@ def get_base_url(request: Request) -> str:
def url_for(request: Request, path: str) -> str: def url_for(request: Request, path: str) -> str:
""" """
Generate a proxy-aware URL for the given path. Generate a proxy-aware URL for the given path.
Args: Args:
request: The current request object request: The current request object
path: The path to generate URL for (should start with /) path: The path to generate URL for (should start with /)
Returns: 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 / # Ensure path starts with /
if not path.startswith("/"): if not path.startswith("/"):
path = "/" + path 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 # Note: config will be imported after parsing CLI args if --config is provided
# For now, we'll delay the import and initialization # For now, we'll delay the import and initialization
......
...@@ -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.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" 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.0", version="0.99.1",
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",
......
...@@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -73,7 +73,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
function restartServer() { function restartServer() {
if (confirm('Are you sure you want to restart the server? This will disconnect all active connections.')) { 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', method: 'POST',
headers: {'Content-Type': 'application/json'} headers: {'Content-Type': 'application/json'}
}) })
......
...@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -24,7 +24,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Date Range Filter Section --> <!-- Date Range Filter Section -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;"> <div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h3 style="margin-bottom: 15px;">Filter by Date Range</h3> <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;"> <div style="flex: 1; min-width: 200px;">
<label style="display: block; margin-bottom: 5px; color: #a0a0a0; font-size: 14px;">Time Range</label> <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;"> <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/>. ...@@ -78,7 +78,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<!-- Filter by Provider/Model/Rotation/Autoselect --> <!-- Filter by Provider/Model/Rotation/Autoselect -->
<div style="background: #1a1a2e; padding: 20px; border-radius: 8px; margin-bottom: 30px;"> <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> <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 --> <!-- Preserve date filter parameters -->
<input type="hidden" name="time_range" value="{{ selected_time_range }}"> <input type="hidden" name="time_range" value="{{ selected_time_range }}">
{% if from_date %}<input type="hidden" name="from_date" value="{{ from_date }}">{% endif %} {% 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/>. ...@@ -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 %} {% if selected_provider or selected_model or selected_rotation or selected_autoselect or selected_user %}
<div> <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 Clear Filters
</a> </a>
</div> </div>
......
...@@ -62,12 +62,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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> <h3 style="margin-top: 30px; margin-bottom: 15px;">Quick Actions</h3>
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="/dashboard/providers" class="btn">Manage Providers</a> <a href="{{ url_for(request, "/dashboard/providers" class="btn">Manage Providers</a>
<a href="/dashboard/rotations" class="btn">Manage Rotations</a> <a href="{{ url_for(request, "/dashboard/rotations" class="btn">Manage Rotations</a>
<a href="/dashboard/autoselect" class="btn">Manage Autoselect</a> <a href="{{ url_for(request, "/dashboard/autoselect" class="btn">Manage Autoselect</a>
<a href="/dashboard/prompts" class="btn">Manage Prompts</a> <a href="{{ url_for(request, "/dashboard/prompts" class="btn">Manage Prompts</a>
<a href="/dashboard/rate-limits" class="btn">Rate Limits</a> <a href="{{ url_for(request, "/dashboard/rate-limits" class="btn">Rate Limits</a>
<a href="/dashboard/response-cache/stats" class="btn">Response Cache</a> <a href="{{ url_for(request, "/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/settings" class="btn btn-secondary">Server Settings</a>
</div> </div>
{% endblock %} {% endblock %}
...@@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -26,7 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div class="alert alert-error">{{ error }}</div> <div class="alert alert-error">{{ error }}</div>
{% endif %} {% endif %}
<form method="POST" action="/dashboard/login"> <form method="POST" action="{{ url_for(request, '/dashboard/login') }}">
<div class="form-group"> <div class="form-group">
<label for="username">Username</label> <label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus> <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/>. ...@@ -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> <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;"> <div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveProviders()">Save Configuration</button> <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> </div>
<script> <script>
...@@ -912,7 +912,7 @@ async function authenticateCodex(key) { ...@@ -912,7 +912,7 @@ async function authenticateCodex(key) {
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Codex OAuth2 Device Authorization flow...</p>'; statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Starting Codex OAuth2 Device Authorization flow...</p>';
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -962,7 +962,7 @@ async function authenticateCodex(key) { ...@@ -962,7 +962,7 @@ async function authenticateCodex(key) {
pollCount++; pollCount++;
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -1015,7 +1015,7 @@ async function checkCodexAuth(key) { ...@@ -1015,7 +1015,7 @@ async function checkCodexAuth(key) {
statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Checking Codex authentication status...</p>'; statusEl.innerHTML = '<p style="margin: 0; color: #4a9eff;">🔄 Checking Codex authentication status...</p>';
try { 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', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -1369,7 +1369,7 @@ async function authenticateClaude(key) { ...@@ -1369,7 +1369,7 @@ async function authenticateClaude(key) {
This extension intercepts localhost OAuth2 callbacks and redirects them to your AISBF server. This extension intercepts localhost OAuth2 callbacks and redirects them to your AISBF server.
</p> </p>
<div style="display: flex; gap: 10px; margin-top: 10px;"> <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 📥 Download Extension
</a> </a>
<button type="button" class="btn btn-secondary" onclick="showExtensionInstructions()" style="padding: 8px 15px; font-size: 13px;"> <button type="button" class="btn btn-secondary" onclick="showExtensionInstructions()" style="padding: 8px 15px; font-size: 13px;">
...@@ -1763,7 +1763,7 @@ function updateProviderCondenseMethod(providerKey, value) { ...@@ -1763,7 +1763,7 @@ function updateProviderCondenseMethod(providerKey, value) {
async function saveProviders() { async function saveProviders() {
try { try {
const response = await fetch('/dashboard/providers', { const response = await fetch('{{ url_for(request, "/dashboard/providers") }}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
...@@ -1772,7 +1772,7 @@ async function saveProviders() { ...@@ -1772,7 +1772,7 @@ async function saveProviders() {
}); });
if (response.ok) { if (response.ok) {
window.location.href = '/dashboard/providers?success=1'; window.location.href = '{{ url_for(request, "/dashboard/providers?success=1") }}';
} else { } else {
alert('Error saving configuration'); alert('Error saving configuration');
} }
......
...@@ -43,7 +43,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -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> <button type="button" class="btn" onclick="addRotation()" style="margin-top: 20px;">Add Rotation</button>
<div style="display: flex; gap: 10px; margin-top: 20px;"> <div style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn" onclick="saveRotations()">Save Configuration</button> <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> </div>
<script> <script>
...@@ -376,7 +376,7 @@ function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelInde ...@@ -376,7 +376,7 @@ function updateRotationModelCondenseMethod(rotationKey, providerIndex, modelInde
async function saveRotations() { async function saveRotations() {
try { try {
const response = await fetch('/dashboard/rotations', { const response = await fetch('{{ url_for(request, "/dashboard/rotations") }}', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
...@@ -385,7 +385,7 @@ async function saveRotations() { ...@@ -385,7 +385,7 @@ async function saveRotations() {
}); });
if (response.ok) { if (response.ok) {
window.location.href = '/dashboard/rotations?success=1'; window.location.href = '{{ url_for(request, "/dashboard/rotations?success=1") }}';
} else { } else {
alert('Error saving configuration'); alert('Error saving configuration');
} }
......
...@@ -508,7 +508,7 @@ function createPersistentService() { ...@@ -508,7 +508,7 @@ function createPersistentService() {
async function checkTorStatus() { async function checkTorStatus() {
try { 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 status = await response.json();
const statusText = document.getElementById('tor-status-text'); const statusText = document.getElementById('tor-status-text');
...@@ -550,7 +550,7 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -550,7 +550,7 @@ document.addEventListener('DOMContentLoaded', function() {
async function refreshCacheStats() { async function refreshCacheStats() {
try { 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 data = await response.json();
const statsText = document.getElementById('cache-stats-text'); const statsText = document.getElementById('cache-stats-text');
...@@ -585,7 +585,7 @@ async function clearResponseCache() { ...@@ -585,7 +585,7 @@ async function clearResponseCache() {
} }
try { 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' method: 'POST'
}); });
const data = await response.json(); const data = await response.json();
......
...@@ -114,7 +114,7 @@ function closeModal() { ...@@ -114,7 +114,7 @@ function closeModal() {
function deleteAutoselect(autoselectName) { function deleteAutoselect(autoselectName) {
if (!confirm(`Are you sure you want to delete the autoselect "${autoselectName}"? This action cannot be undone.`)) return; 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', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
...@@ -161,7 +161,7 @@ document.getElementById('autoselect-form').addEventListener('submit', function(e ...@@ -161,7 +161,7 @@ document.getElementById('autoselect-form').addEventListener('submit', function(e
formData.append('autoselect_name', autoselectName); formData.append('autoselect_name', autoselectName);
formData.append('autoselect_config', JSON.stringify(configObj)); 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'; const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, { fetch(url, {
......
...@@ -156,7 +156,7 @@ function closeModal() { ...@@ -156,7 +156,7 @@ function closeModal() {
function deleteProvider(providerName) { function deleteProvider(providerName) {
if (!confirm(`Are you sure you want to delete the provider "${providerName}"? This action cannot be undone.`)) return; 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', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
...@@ -203,7 +203,7 @@ document.getElementById('provider-form').addEventListener('submit', function(e) ...@@ -203,7 +203,7 @@ document.getElementById('provider-form').addEventListener('submit', function(e)
formData.append('provider_name', providerName); formData.append('provider_name', providerName);
formData.append('provider_config', JSON.stringify(configObj)); 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'; const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, { fetch(url, {
...@@ -347,7 +347,7 @@ async function loadAuthFiles(providerId) { ...@@ -347,7 +347,7 @@ async function loadAuthFiles(providerId) {
<span style="color: #a0a0a0; font-size: 0.9em;"> (${formatFileSize(file.file_size)})</span> <span style="color: #a0a0a0; font-size: 0.9em;"> (${formatFileSize(file.file_size)})</span>
</span> </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> <button class="btn btn-danger btn-sm" onclick="deleteAuthFile('${providerId}', '${file.file_type}')">Delete</button>
</span> </span>
</div> </div>
......
...@@ -114,7 +114,7 @@ function closeModal() { ...@@ -114,7 +114,7 @@ function closeModal() {
function deleteRotation(rotationName) { function deleteRotation(rotationName) {
if (!confirm(`Are you sure you want to delete the rotation "${rotationName}"? This action cannot be undone.`)) return; 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', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
...@@ -161,7 +161,7 @@ document.getElementById('rotation-form').addEventListener('submit', function(e) ...@@ -161,7 +161,7 @@ document.getElementById('rotation-form').addEventListener('submit', function(e)
formData.append('rotation_name', rotationName); formData.append('rotation_name', rotationName);
formData.append('rotation_config', JSON.stringify(configObj)); 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'; const method = currentEditingIndex >= 0 ? 'PUT' : 'POST';
fetch(url, { fetch(url, {
......
...@@ -181,7 +181,7 @@ function closeModal() { ...@@ -181,7 +181,7 @@ function closeModal() {
function deleteToken(tokenId) { 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; 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', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
...@@ -228,7 +228,7 @@ document.getElementById('create-token-form').addEventListener('submit', function ...@@ -228,7 +228,7 @@ document.getElementById('create-token-form').addEventListener('submit', function
formData.append('description', description); formData.append('description', description);
} }
fetch('{{ url_for(request, "/dashboard/user/tokens") }}', { fetch('{{ url_for(request, "{{ url_for(request, "/dashboard/user/tokens") }}', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then(response => { }).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