mirror of
https://github.com/dobin/detonator
synced 2026-08-09 12:31:13 +00:00
refactor: (deepseekv4) login with cookies (detonatorui), remove CORS
This commit is contained in:
@@ -16,10 +16,7 @@ from detonatorapi.logging_config import setup_logging
|
||||
from detonatorapi.fastapi_app import app as fastapi_app
|
||||
from detonatorapi.connectors.connectors import connectors
|
||||
from detonatorapi.database import File
|
||||
|
||||
from detonatorapi.settings import CORS_ALLOW_ORIGINS
|
||||
from detonatorui.config import API_BASE_URL
|
||||
|
||||
from detonatorapi.database import get_db_direct
|
||||
from detonatorapi.db_interface import db_list_profiles
|
||||
|
||||
@@ -85,24 +82,6 @@ def parse_arguments():
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def print_cors_help():
|
||||
"""Print helpful information about CORS configuration."""
|
||||
print("\n" + "="*70)
|
||||
print("⚠️ CORS CONFIGURATION")
|
||||
print("="*70)
|
||||
print(f"\nThe Browser UI JavaScript will attempt to connect to the API:")
|
||||
print(f" - {API_BASE_URL}")
|
||||
print(f"")
|
||||
print(f"Where the API current CORS allowed value:")
|
||||
print(f" - {CORS_ALLOW_ORIGINS}")
|
||||
print(f"")
|
||||
print(f"To change CORS settings:")
|
||||
print(f" Option A - Environment variable:")
|
||||
print(f" export DETONATOR_CORS_ORIGINS='http://detonator.r00ted.ch'")
|
||||
print(f" Option B - Edit detonatorapi/settings.py:")
|
||||
print(f" CORS_ALLOW_ORIGINS = [ 'http://detonator.r00ted.ch' ] ")
|
||||
print("\n" + "="*70 + "\n")
|
||||
|
||||
|
||||
def refresh_files_from_disk():
|
||||
# get a list of filenames of files in upload/
|
||||
@@ -167,7 +146,6 @@ def main():
|
||||
start_web = args.mode in ['both', 'web']
|
||||
|
||||
fastapi_thread = None
|
||||
print_cors_help()
|
||||
|
||||
# check if we have a profile with data.edr_mde configured
|
||||
db = get_db_direct()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import logging
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -16,7 +15,7 @@ from .web_files import router as files_router
|
||||
from .web_submissions import router as submissions_router
|
||||
from .web_vms import router as vms_router
|
||||
from .web_profiles import router as profiles_router
|
||||
from .settings import CORS_ALLOW_ORIGINS, AUTH_PASSWORD
|
||||
from .settings import AUTH_PASSWORD
|
||||
from .utils import sanitize_runtime_seconds
|
||||
from .edr_cloud.edr_cloud_manager import edr_cloud_plugins
|
||||
from .web_files import MAX_FILE_SIZE
|
||||
@@ -32,16 +31,6 @@ logger = logging.getLogger(__name__)
|
||||
app = FastAPI(title="Detonator API", version="0.1.0")
|
||||
|
||||
|
||||
# Add CORS middleware to allow requests from Flask frontend
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=CORS_ALLOW_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Initialize VM manager and start monitoring on startup"""
|
||||
|
||||
@@ -19,8 +19,4 @@ _settings = load_settings()
|
||||
|
||||
VM_DESTROY_AFTER = _settings.get("vm_destroy_after", 60) # minutes
|
||||
AUTH_PASSWORD = _settings.get("auth_password", "")
|
||||
CORS_ALLOW_ORIGINS = _settings.get(
|
||||
"cors_allowed_origins",
|
||||
"http://localhost:5000,http://127.0.0.1:5000"
|
||||
).split(",")
|
||||
DISABLE_REVERT_VM = _settings.get("disable_revert_vm", False)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
auth_password: ""
|
||||
cors_allowed_origins: http://localhost:5000,http://127.0.0.1:5000
|
||||
|
||||
vm_destroy_after: 60
|
||||
disable_revert_vm: false
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
"""
|
||||
Authentication module for Detonator API.
|
||||
|
||||
Two authentication paths are supported:
|
||||
|
||||
1. Browser → Flask (session cookie) → FastAPI (X-Auth-Password header)
|
||||
- Flask validates the user's password, sets a signed session cookie.
|
||||
- Flask proxies API calls to FastAPI, injecting X-Auth-Password with the
|
||||
server-side AUTH_PASSWORD value.
|
||||
- The browser never sees or stores the raw password.
|
||||
|
||||
2. curl / direct API → FastAPI (X-Auth-Password or Authorization header)
|
||||
- The user provides the password directly via X-Auth-Password header,
|
||||
Authorization: Bearer <password>, or Authorization: Basic <base64>.
|
||||
- Useful for scripting and direct API access.
|
||||
|
||||
When AUTH_PASSWORD is empty/None, authentication is disabled and all
|
||||
requests are treated as admin.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from fastapi import Request, HTTPException
|
||||
@@ -14,6 +34,7 @@ def check_password_auth(request: Request) -> bool:
|
||||
return True
|
||||
|
||||
# Check for X-Auth-Password header
|
||||
# As used by DetonatorUi
|
||||
auth_password = request.headers.get("X-Auth-Password", "")
|
||||
if hmac.compare_digest(auth_password, AUTH_PASSWORD):
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from flask import session
|
||||
|
||||
from detonatorapi.settings import AUTH_PASSWORD
|
||||
|
||||
|
||||
def is_auth_enabled() -> bool:
|
||||
"""Check if authentication is enabled (AUTH_PASSWORD is configured)."""
|
||||
if AUTH_PASSWORD and AUTH_PASSWORD != "":
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def api_headers() -> dict:
|
||||
"""Return headers for proxying requests to the FastAPI backend.
|
||||
|
||||
Only includes X-Auth-Password if the user session is authenticated.
|
||||
Uses the server-side AUTH_PASSWORD, so the browser never sees it.
|
||||
"""
|
||||
headers = {}
|
||||
if AUTH_PASSWORD and session.get("authenticated"):
|
||||
headers["X-Auth-Password"] = AUTH_PASSWORD
|
||||
return headers
|
||||
@@ -1,6 +1,7 @@
|
||||
|
||||
import os
|
||||
import yaml
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@@ -16,3 +17,5 @@ def load_config():
|
||||
_config = load_config()
|
||||
|
||||
API_BASE_URL = _config.get("api_base_url", "http://localhost:8000")
|
||||
SECRET_KEY = _config.get("secret_key") or secrets.token_hex(32)
|
||||
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
api_base_url: "http://localhost:8000"
|
||||
|
||||
# Flask session signing key. Change this in production.
|
||||
# If not set, a random key is generated on each startup (invalidating existing sessions).
|
||||
secret_key: ""
|
||||
|
||||
+41
-12
@@ -1,19 +1,23 @@
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session
|
||||
from functools import wraps
|
||||
import requests
|
||||
import os
|
||||
import logging
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from .post import post_bp
|
||||
from .get import get_bp
|
||||
from .config import API_BASE_URL
|
||||
from .config import API_BASE_URL, SECRET_KEY
|
||||
from detonatorapi.settings import AUTH_PASSWORD
|
||||
from detonatorapi.edr_cloud.elastic_rule_resolver import ElasticRuleResolver
|
||||
|
||||
from .auth import is_auth_enabled, api_headers
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "detonator-secret-key" # Change this in production
|
||||
app.secret_key = SECRET_KEY
|
||||
app.config['MAX_CONTENT_LENGTH'] = 128 * 1024 * 1024 # 128 MB
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
|
||||
|
||||
@app.errorhandler(413)
|
||||
@@ -35,20 +39,45 @@ csv_path = Path(__file__).parent.parent / "elastic_rules" / "elastic_rules.csv"
|
||||
elastic_rule_resolver = ElasticRuleResolver(csv_path=str(csv_path))
|
||||
|
||||
|
||||
# Make API_BASE_URL available to all templates
|
||||
@app.context_processor
|
||||
def inject_api_base_url():
|
||||
# Check if authentication is enabled
|
||||
auth_enabled = False
|
||||
if AUTH_PASSWORD != None and AUTH_PASSWORD != "":
|
||||
auth_enabled = True
|
||||
|
||||
def inject_template_globals():
|
||||
return {
|
||||
'API_BASE_URL': API_BASE_URL,
|
||||
'AUTH_ENABLED': auth_enabled
|
||||
'AUTH_ENABLED': is_auth_enabled(),
|
||||
'IS_AUTHENTICATED': not is_auth_enabled() or session.get("authenticated", False),
|
||||
}
|
||||
|
||||
|
||||
# Authentication
|
||||
|
||||
|
||||
|
||||
@app.route("/login", methods=["POST"])
|
||||
def login_post():
|
||||
"""Validate password and set session cookie."""
|
||||
password = request.form.get("password", "")
|
||||
if not is_auth_enabled():
|
||||
session["authenticated"] = True
|
||||
return redirect(request.args.get("return_url", "/"))
|
||||
|
||||
if AUTH_PASSWORD and hmac.compare_digest(password, AUTH_PASSWORD):
|
||||
session["authenticated"] = True
|
||||
session.permanent = True
|
||||
return redirect(request.args.get("return_url") or "/")
|
||||
|
||||
flash("Invalid password. Please try again.", "error")
|
||||
return redirect(url_for("get.login_page", return_url=request.args.get("return_url")))
|
||||
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
"""Clear the session and redirect to login page."""
|
||||
session.clear()
|
||||
if is_auth_enabled():
|
||||
return redirect(url_for("get.login_page"))
|
||||
return redirect(url_for("get.index"))
|
||||
|
||||
|
||||
# Helper function for Jinja2 templates
|
||||
def get_status_color(status):
|
||||
"""Get CSS classes for status badges"""
|
||||
|
||||
+24
-31
@@ -2,23 +2,17 @@ from flask import Blueprint, render_template, request, jsonify, redirect, url_f
|
||||
from typing import Optional, Dict
|
||||
import requests
|
||||
import logging
|
||||
from .config import API_BASE_URL
|
||||
import json
|
||||
import logging
|
||||
|
||||
from .config import API_BASE_URL
|
||||
from .auth import api_headers, is_auth_enabled
|
||||
|
||||
|
||||
get_bp = Blueprint('get', __name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _auth_headers() -> Dict[str, str]:
|
||||
headers: Dict[str, str] = {}
|
||||
password = request.headers.get("X-Auth-Password")
|
||||
if password:
|
||||
headers["X-Auth-Password"] = password
|
||||
return headers
|
||||
|
||||
|
||||
# MAIN Pages
|
||||
|
||||
@get_bp.route("/")
|
||||
@@ -31,7 +25,6 @@ def login_page():
|
||||
|
||||
@get_bp.route("/logout")
|
||||
def logout_page():
|
||||
# This will be handled by JavaScript to clear localStorage
|
||||
return render_template("logout.html")
|
||||
|
||||
@get_bp.route("/files")
|
||||
@@ -43,7 +36,7 @@ def create_file_submission_page(file_id):
|
||||
"""Page to create a submission for a specific file"""
|
||||
try:
|
||||
# Fetch file details
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}", headers=_auth_headers())
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}", headers=api_headers())
|
||||
if file_response.status_code == 200:
|
||||
file_data = file_response.json()
|
||||
else:
|
||||
@@ -52,7 +45,7 @@ def create_file_submission_page(file_id):
|
||||
return redirect(url_for('get.files_page'))
|
||||
|
||||
# Fetch profiles
|
||||
profiles_response = requests.get(f"{API_BASE_URL}/api/profiles", headers=_auth_headers())
|
||||
profiles_response = requests.get(f"{API_BASE_URL}/api/profiles", headers=api_headers())
|
||||
if profiles_response.status_code == 200:
|
||||
profiles = profiles_response.json()
|
||||
else:
|
||||
@@ -70,7 +63,7 @@ def edit_file_page(file_id):
|
||||
"""Page to edit a file's metadata"""
|
||||
try:
|
||||
# Fetch file details
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}", headers=_auth_headers())
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}", headers=api_headers())
|
||||
if file_response.status_code == 200:
|
||||
file_data = file_response.json()
|
||||
else:
|
||||
@@ -92,7 +85,7 @@ def submissions_page():
|
||||
def submission_detail_page(submission_id):
|
||||
"""Page to display details of a specific submission"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions/{submission_id}", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions/{submission_id}", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
submission = response.json()
|
||||
else:
|
||||
@@ -108,7 +101,7 @@ def submission_detail_page(submission_id):
|
||||
def submission_page():
|
||||
# Fetch profiles list
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
profiles = response.json()
|
||||
else:
|
||||
@@ -137,7 +130,7 @@ def view_profile_page(profile_id):
|
||||
"""Page to view a specific profile (read-only)"""
|
||||
try:
|
||||
# Fetch the profile data
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles/{profile_id}", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles/{profile_id}", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
profile = response.json()
|
||||
else:
|
||||
@@ -146,7 +139,7 @@ def view_profile_page(profile_id):
|
||||
return redirect(url_for('get.profiles_page'))
|
||||
|
||||
# Fetch available connectors
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=_auth_headers())
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=api_headers())
|
||||
if connectors_response.status_code == 200:
|
||||
connectors = connectors_response.json()
|
||||
else:
|
||||
@@ -168,7 +161,7 @@ def edit_profile_page(profile_id):
|
||||
"""Page to edit a specific profile"""
|
||||
try:
|
||||
# Fetch the profile data
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles/{profile_id}", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles/{profile_id}", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
profile = response.json()
|
||||
else:
|
||||
@@ -177,7 +170,7 @@ def edit_profile_page(profile_id):
|
||||
return redirect(url_for('get.profiles_page'))
|
||||
|
||||
# Fetch available connectors
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=_auth_headers())
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=api_headers())
|
||||
if connectors_response.status_code == 200:
|
||||
connectors = connectors_response.json()
|
||||
else:
|
||||
@@ -198,7 +191,7 @@ def create_profile_page():
|
||||
"""Page to create a new profile"""
|
||||
try:
|
||||
# Fetch available connectors
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=_auth_headers())
|
||||
connectors_response = requests.get(f"{API_BASE_URL}/api/connectors", headers=api_headers())
|
||||
if connectors_response.status_code == 200:
|
||||
connectors = connectors_response.json()
|
||||
else:
|
||||
@@ -244,7 +237,7 @@ def semidatasieve(submission_id):
|
||||
def files_template():
|
||||
"""Template endpoint to render files list via HTMX"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/files", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/files", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
files = response.json()
|
||||
# Sort submissions by ID in descending order (newest first)
|
||||
@@ -296,7 +289,7 @@ def submissions_template():
|
||||
if filter_status and filter_status != 'all':
|
||||
params['status'] = filter_status
|
||||
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions", params=params, headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions", params=params, headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
submissions = response.json()
|
||||
else:
|
||||
@@ -311,7 +304,7 @@ def submission_details_template(submission_id):
|
||||
"""Template endpoint to render submission details via HTMX"""
|
||||
submission: Optional[Dict] = {}
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions/{submission_id}", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions/{submission_id}", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
submission = response.json()
|
||||
else:
|
||||
@@ -326,7 +319,7 @@ def submission_details_template(submission_id):
|
||||
def vms_template():
|
||||
"""Template endpoint to render VMs list via HTMX"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/vms", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/vms", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
vms = response.json()
|
||||
vms = sorted(vms, key=lambda vm: vm['name'])
|
||||
@@ -341,7 +334,7 @@ def vms_template():
|
||||
def profiles_template():
|
||||
"""Template endpoint to render profiles list via HTMX"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
profiles = response.json()
|
||||
|
||||
@@ -349,7 +342,7 @@ def profiles_template():
|
||||
for profile_name, profile in profiles.items():
|
||||
url = f"{API_BASE_URL}/api/profiles/{profile['id']}/status"
|
||||
try:
|
||||
status_response = requests.get(url, headers=_auth_headers())
|
||||
status_response = requests.get(url, headers=api_headers())
|
||||
if status_response.status_code == 200:
|
||||
status_data = status_response.json()
|
||||
profile['agent_alive'] = status_data.get('agent_alive', False)
|
||||
@@ -377,7 +370,7 @@ def profiles_template():
|
||||
def profiles_overview_template():
|
||||
"""Template endpoint to render profiles overview for index page via HTMX"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles")
|
||||
response = requests.get(f"{API_BASE_URL}/api/profiles", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
templates = response.json()
|
||||
# Add the name to each template for easier access in templates
|
||||
@@ -430,7 +423,7 @@ def submissions_table_template():
|
||||
if filter_status and filter_status != 'all':
|
||||
params['status'] = filter_status
|
||||
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions", params=params)
|
||||
response = requests.get(f"{API_BASE_URL}/api/submissions", params=params, headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
submissions = response.json()
|
||||
else:
|
||||
@@ -446,14 +439,14 @@ def create_file_submission_template(file_id):
|
||||
"""Template endpoint to render submission creation form via HTMX"""
|
||||
try:
|
||||
# Fetch file details
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}")
|
||||
file_response = requests.get(f"{API_BASE_URL}/api/files/{file_id}", headers=api_headers())
|
||||
if file_response.status_code == 200:
|
||||
file_data = file_response.json()
|
||||
else:
|
||||
file_data = None
|
||||
|
||||
# Fetch profiles
|
||||
profiles_response = requests.get(f"{API_BASE_URL}/api/profiles")
|
||||
profiles_response = requests.get(f"{API_BASE_URL}/api/profiles", headers=api_headers())
|
||||
if profiles_response.status_code == 200:
|
||||
profiles = profiles_response.json()
|
||||
else:
|
||||
@@ -469,7 +462,7 @@ def create_file_submission_template(file_id):
|
||||
def get_connector_info(connector_name):
|
||||
"""Get connector information for display"""
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/api/connectors", headers=_auth_headers())
|
||||
response = requests.get(f"{API_BASE_URL}/api/connectors", headers=api_headers())
|
||||
if response.status_code == 200:
|
||||
connectors = response.json()
|
||||
if connector_name in connectors:
|
||||
|
||||
+126
-73
@@ -1,8 +1,10 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, session, redirect, url_for
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
|
||||
from .config import API_BASE_URL
|
||||
from .auth import api_headers, is_auth_enabled
|
||||
|
||||
from detonatorapi.utils import (
|
||||
filename_randomizer,
|
||||
@@ -16,6 +18,13 @@ logger = logging.getLogger(__name__)
|
||||
post_bp = Blueprint('post', __name__)
|
||||
|
||||
|
||||
@post_bp.before_request
|
||||
def require_auth_for_post():
|
||||
"""Require authentication for all POST routes in this blueprint."""
|
||||
if is_auth_enabled() and not session.get("authenticated"):
|
||||
return jsonify({"error": "Authentication required. Please log in.", "auth_required": True}), 401
|
||||
|
||||
|
||||
def handle_api_response(response, operation_name="operation"):
|
||||
"""Helper function to handle API responses consistently"""
|
||||
if response.status_code == 200:
|
||||
@@ -45,50 +54,43 @@ def create_submission():
|
||||
try:
|
||||
files = {}
|
||||
data = {}
|
||||
|
||||
# Forward authentication header from request
|
||||
headers = {}
|
||||
if 'X-Auth-Password' in request.headers:
|
||||
headers['X-Auth-Password'] = request.headers.get('X-Auth-Password')
|
||||
elif 'Authorization' in request.headers:
|
||||
headers['Authorization'] = request.headers.get('Authorization')
|
||||
|
||||
|
||||
if 'file' in request.files:
|
||||
# fix filename handling
|
||||
uploaded_file = request.files['file']
|
||||
filename = uploaded_file.filename
|
||||
|
||||
|
||||
# Check if filename randomization is enabled
|
||||
randomize = request.form.get('randomize_filename') == 'on'
|
||||
if randomize and filename:
|
||||
filename = filename_randomizer(filename)
|
||||
|
||||
|
||||
files['file'] = (filename, uploaded_file.stream, uploaded_file.content_type)
|
||||
|
||||
|
||||
if 'source_url' in request.form:
|
||||
data['source_url'] = request.form['source_url']
|
||||
|
||||
|
||||
if 'file_comment' in request.form:
|
||||
data['file_comment'] = request.form['file_comment']
|
||||
|
||||
|
||||
if 'submission_comment' in request.form:
|
||||
data['submission_comment'] = request.form['submission_comment']
|
||||
|
||||
|
||||
if 'exec_arguments' in request.form:
|
||||
data['exec_arguments'] = request.form['exec_arguments']
|
||||
|
||||
|
||||
if 'project' in request.form:
|
||||
data['project'] = request.form['project']
|
||||
|
||||
|
||||
if 'profile_name' in request.form:
|
||||
data['profile_name'] = request.form['profile_name']
|
||||
|
||||
|
||||
if 'password' in request.form:
|
||||
data['password'] = request.form['password']
|
||||
|
||||
if 'token' in request.form:
|
||||
data['token'] = request.form['token']
|
||||
|
||||
|
||||
if 'runtime' in request.form:
|
||||
raw_runtime = request.form['runtime'].strip()
|
||||
if raw_runtime:
|
||||
@@ -101,14 +103,14 @@ def create_submission():
|
||||
"error": f"Runtime must be between {RUNTIME_MIN_SECONDS} and {RUNTIME_MAX_SECONDS} seconds"
|
||||
}, 400
|
||||
data['runtime'] = runtime_value
|
||||
|
||||
|
||||
if 'drop_path' in request.form:
|
||||
data['drop_path'] = request.form['drop_path']
|
||||
|
||||
|
||||
if 'execution_mode' in request.form:
|
||||
data['execution_mode'] = request.form['execution_mode']
|
||||
|
||||
response = requests.post(f"{API_BASE_URL}/api/create-submission", files=files, data=data, headers=headers)
|
||||
|
||||
response = requests.post(f"{API_BASE_URL}/api/create-submission", files=files, data=data, headers=api_headers())
|
||||
return handle_api_response(response, "file upload and submission")
|
||||
except requests.RequestException as e:
|
||||
return {"error": f"Could not upload file: {str(e)}"}, 500
|
||||
@@ -118,16 +120,9 @@ def create_submission():
|
||||
def create_profile():
|
||||
"""Proxy endpoint to create a profile via FastAPI"""
|
||||
try:
|
||||
# Forward authentication header from request
|
||||
headers = {}
|
||||
if 'X-Auth-Password' in request.headers:
|
||||
headers['X-Auth-Password'] = request.headers.get('X-Auth-Password')
|
||||
elif 'Authorization' in request.headers:
|
||||
headers['Authorization'] = request.headers.get('Authorization')
|
||||
|
||||
# Prepare form data
|
||||
data = {}
|
||||
|
||||
|
||||
# Required fields
|
||||
if 'name' in request.form:
|
||||
data['name'] = request.form['name']
|
||||
@@ -139,7 +134,7 @@ def create_profile():
|
||||
data['port'] = request.form['port']
|
||||
if 'data' in request.form:
|
||||
data['data'] = request.form['data']
|
||||
|
||||
|
||||
# Optional fields
|
||||
if 'default_drop_path' in request.form:
|
||||
data['default_drop_path'] = request.form['default_drop_path']
|
||||
@@ -149,23 +144,23 @@ def create_profile():
|
||||
data['password'] = request.form['password']
|
||||
if 'rededr_port' in request.form:
|
||||
data['rededr_port'] = request.form['rededr_port']
|
||||
|
||||
|
||||
# Send POST request to FastAPI
|
||||
response = requests.post(
|
||||
f"{API_BASE_URL}/api/profiles",
|
||||
data=data,
|
||||
headers=headers
|
||||
f"{API_BASE_URL}/api/profiles",
|
||||
data=data,
|
||||
headers=api_headers()
|
||||
)
|
||||
|
||||
|
||||
result = handle_api_response(response, "profile creation")
|
||||
|
||||
|
||||
# If handle_api_response returned a tuple (error case), return it
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
|
||||
|
||||
# Success case - return success message
|
||||
return jsonify({"message": "Profile created successfully", "profile": result}), 200
|
||||
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while creating profile: {e}")
|
||||
return jsonify({"error": f"Could not create profile: {str(e)}"}), 500
|
||||
@@ -175,16 +170,9 @@ def create_profile():
|
||||
def update_profile(profile_id):
|
||||
"""Proxy endpoint to update a profile via FastAPI"""
|
||||
try:
|
||||
# Forward authentication header from request
|
||||
headers = {}
|
||||
if 'X-Auth-Password' in request.headers:
|
||||
headers['X-Auth-Password'] = request.headers.get('X-Auth-Password')
|
||||
elif 'Authorization' in request.headers:
|
||||
headers['Authorization'] = request.headers.get('Authorization')
|
||||
|
||||
# Prepare form data
|
||||
data = {}
|
||||
|
||||
|
||||
# Required fields
|
||||
if 'name' in request.form:
|
||||
data['name'] = request.form['name']
|
||||
@@ -196,7 +184,7 @@ def update_profile(profile_id):
|
||||
data['port'] = request.form['port']
|
||||
if 'data' in request.form:
|
||||
data['data'] = request.form['data']
|
||||
|
||||
|
||||
# Optional fields
|
||||
if 'default_drop_path' in request.form:
|
||||
data['default_drop_path'] = request.form['default_drop_path']
|
||||
@@ -206,23 +194,23 @@ def update_profile(profile_id):
|
||||
data['password'] = request.form['password']
|
||||
if 'rededr_port' in request.form:
|
||||
data['rededr_port'] = request.form['rededr_port']
|
||||
|
||||
|
||||
# Send PUT request to FastAPI
|
||||
response = requests.put(
|
||||
f"{API_BASE_URL}/api/profiles/{profile_id}",
|
||||
data=data,
|
||||
headers=headers
|
||||
f"{API_BASE_URL}/api/profiles/{profile_id}",
|
||||
data=data,
|
||||
headers=api_headers()
|
||||
)
|
||||
|
||||
|
||||
result = handle_api_response(response, "profile update")
|
||||
|
||||
|
||||
# If handle_api_response returned a tuple (error case), return it
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
|
||||
|
||||
# Success case - return success message
|
||||
return jsonify({"message": "Profile updated successfully", "profile": result}), 200
|
||||
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while updating profile {profile_id}: {e}")
|
||||
return jsonify({"error": f"Could not update profile: {str(e)}"}), 500
|
||||
@@ -232,39 +220,104 @@ def update_profile(profile_id):
|
||||
def update_file(file_id):
|
||||
"""Proxy endpoint to update a file's metadata via FastAPI"""
|
||||
try:
|
||||
# Forward authentication header from request
|
||||
headers = {}
|
||||
if 'X-Auth-Password' in request.headers:
|
||||
headers['X-Auth-Password'] = request.headers.get('X-Auth-Password')
|
||||
elif 'Authorization' in request.headers:
|
||||
headers['Authorization'] = request.headers.get('Authorization')
|
||||
|
||||
# Prepare form data
|
||||
data = {}
|
||||
|
||||
|
||||
if 'source_url' in request.form:
|
||||
data['source_url'] = request.form['source_url']
|
||||
if 'comment' in request.form:
|
||||
data['comment'] = request.form['comment']
|
||||
if 'exec_arguments' in request.form:
|
||||
data['exec_arguments'] = request.form['exec_arguments']
|
||||
|
||||
|
||||
# Send PUT request to FastAPI
|
||||
response = requests.put(
|
||||
f"{API_BASE_URL}/api/files/{file_id}",
|
||||
data=data,
|
||||
headers=headers
|
||||
f"{API_BASE_URL}/api/files/{file_id}",
|
||||
data=data,
|
||||
headers=api_headers()
|
||||
)
|
||||
|
||||
|
||||
result = handle_api_response(response, "file update")
|
||||
|
||||
|
||||
# If handle_api_response returned a tuple (error case), return it
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
|
||||
|
||||
# Success case - return success message
|
||||
return jsonify({"message": "File updated successfully", "file": result}), 200
|
||||
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while updating file {file_id}: {e}")
|
||||
return jsonify({"error": f"Could not update file: {str(e)}"}), 500
|
||||
|
||||
|
||||
# --- Admin action proxies (called by templates via fetch) ---
|
||||
|
||||
@post_bp.route("/api/profiles/<int:profile_id>/delete", methods=["POST", "DELETE"])
|
||||
def delete_profile(profile_id):
|
||||
"""Proxy endpoint to delete a profile via FastAPI"""
|
||||
try:
|
||||
response = requests.delete(f"{API_BASE_URL}/api/profiles/{profile_id}", headers=api_headers())
|
||||
result = handle_api_response(response, "profile deletion")
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
return jsonify(result), response.status_code
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while deleting profile {profile_id}: {e}")
|
||||
return jsonify({"error": f"Could not delete profile: {str(e)}"}), 500
|
||||
|
||||
|
||||
@post_bp.route("/api/profiles/<int:profile_id>/release_lock", methods=["POST"])
|
||||
def release_lock(profile_id):
|
||||
"""Proxy endpoint to release a profile lock via FastAPI"""
|
||||
try:
|
||||
response = requests.post(f"{API_BASE_URL}/api/profiles/{profile_id}/release_lock", headers=api_headers())
|
||||
result = handle_api_response(response, "lock release")
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
return jsonify(result), response.status_code
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while releasing lock for profile {profile_id}: {e}")
|
||||
return jsonify({"error": f"Could not release lock: {str(e)}"}), 500
|
||||
|
||||
|
||||
@post_bp.route("/api/profiles/<int:profile_id>/reboot", methods=["POST"])
|
||||
def reboot_profile(profile_id):
|
||||
"""Proxy endpoint to reboot a profile's VM via FastAPI"""
|
||||
try:
|
||||
response = requests.post(f"{API_BASE_URL}/api/profiles/{profile_id}/reboot", headers=api_headers())
|
||||
result = handle_api_response(response, "VM reboot")
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
return jsonify(result), response.status_code
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while rebooting profile {profile_id}: {e}")
|
||||
return jsonify({"error": f"Could not reboot VM: {str(e)}"}), 500
|
||||
|
||||
|
||||
@post_bp.route("/api/profiles/<int:profile_id>/revert", methods=["POST"])
|
||||
def revert_profile(profile_id):
|
||||
"""Proxy endpoint to revert a profile's VM via FastAPI"""
|
||||
try:
|
||||
response = requests.post(f"{API_BASE_URL}/api/profiles/{profile_id}/revert", headers=api_headers())
|
||||
result = handle_api_response(response, "VM revert")
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
return jsonify(result), response.status_code
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while reverting profile {profile_id}: {e}")
|
||||
return jsonify({"error": f"Could not revert VM: {str(e)}"}), 500
|
||||
|
||||
|
||||
@post_bp.route("/api/submissions/<int:submission_id>/delete", methods=["POST", "DELETE"])
|
||||
def delete_submission(submission_id):
|
||||
"""Proxy endpoint to delete a submission via FastAPI"""
|
||||
try:
|
||||
response = requests.delete(f"{API_BASE_URL}/api/submissions/{submission_id}", headers=api_headers())
|
||||
result = handle_api_response(response, "submission deletion")
|
||||
if isinstance(result, tuple):
|
||||
return result
|
||||
return jsonify(result), response.status_code
|
||||
except requests.RequestException as e:
|
||||
logger.exception(f"Exception while deleting submission {submission_id}: {e}")
|
||||
return jsonify({"error": f"Could not delete submission: {str(e)}"}), 500
|
||||
|
||||
@@ -104,8 +104,20 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logout button at bottom -->
|
||||
<div class="p-4 border-t nav-border">
|
||||
<!-- Login button at bottom (shown when not authenticated) -->
|
||||
<div class="p-4 border-t nav-border" data-auth-hide>
|
||||
<a href="/login" class="nav-item block px-6 py-3 text-white rounded">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
Login
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Logout button at bottom (shown when authenticated) -->
|
||||
<div class="p-4 border-t nav-border" data-auth-required>
|
||||
<a href="/logout" class="nav-item block px-6 py-3 text-white rounded">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
@@ -123,21 +135,15 @@
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Authentication helper functions
|
||||
// Authentication helper - uses server-side session status
|
||||
function isAuthenticated() {
|
||||
// If no auth is configured, always return true
|
||||
if (! {{ AUTH_ENABLED|lower }}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise check for stored password
|
||||
return !!localStorage.getItem('detonator_auth_password');
|
||||
return {{ IS_AUTHENTICATED|lower }};
|
||||
}
|
||||
|
||||
// Show/hide elements based on authentication status
|
||||
function updateUIForAuth() {
|
||||
const authenticated = isAuthenticated();
|
||||
|
||||
|
||||
// Show/hide elements with data-auth-required attribute
|
||||
document.querySelectorAll('[data-auth-required]').forEach(el => {
|
||||
if (authenticated) {
|
||||
@@ -148,7 +154,7 @@
|
||||
el.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Hide elements with data-auth-hide attribute when authenticated
|
||||
document.querySelectorAll('[data-auth-hide]').forEach(el => {
|
||||
if (authenticated) {
|
||||
@@ -163,45 +169,26 @@
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', updateUIForAuth);
|
||||
|
||||
|
||||
// Run after HTMX updates
|
||||
document.body.addEventListener('htmx:afterSwap', updateUIForAuth);
|
||||
document.body.addEventListener('htmx:afterSettle', updateUIForAuth);
|
||||
|
||||
// Authentication interceptor for HTMX requests
|
||||
document.body.addEventListener('htmx:configRequest', function(evt) {
|
||||
const password = localStorage.getItem('detonator_auth_password');
|
||||
if (password) {
|
||||
evt.detail.headers['X-Auth-Password'] = password;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle 401 responses by redirecting to login
|
||||
document.body.addEventListener('htmx:responseError', function(evt) {
|
||||
if (evt.detail.xhr.status === 401) {
|
||||
// Store current page to return after login
|
||||
const returnUrl = encodeURIComponent(window.location.pathname);
|
||||
window.location.href = '/login?return=' + returnUrl;
|
||||
var returnUrl = encodeURIComponent(window.location.pathname);
|
||||
window.location.href = '/login?return_url=' + returnUrl;
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle regular fetch requests (non-HTMX)
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(...args) {
|
||||
// Add auth header to fetch requests
|
||||
const password = localStorage.getItem('detonator_auth_password');
|
||||
if (password && args[1]) {
|
||||
args[1].headers = args[1].headers || {};
|
||||
args[1].headers['X-Auth-Password'] = password;
|
||||
} else if (password && !args[1]) {
|
||||
args[1] = { headers: { 'X-Auth-Password': password } };
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, args).then(response => {
|
||||
// Redirect to login on 401
|
||||
// Fetch interceptor: redirect to login on 401
|
||||
var originalFetch = window.fetch;
|
||||
window.fetch = function() {
|
||||
return originalFetch.apply(this, arguments).then(function(response) {
|
||||
if (response.status === 401) {
|
||||
const returnUrl = encodeURIComponent(window.location.pathname);
|
||||
window.location.href = '/login?return=' + returnUrl;
|
||||
var returnUrl = encodeURIComponent(window.location.pathname);
|
||||
window.location.href = '/login?return_url=' + returnUrl;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -45,30 +45,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Check API status on page load
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const apiUrl = '{{ API_BASE_URL }}';
|
||||
const checkingDiv = document.getElementById('api-status-checking');
|
||||
const errorDiv = document.getElementById('api-status-error');
|
||||
const urlDisplay = document.getElementById('api-url-display');
|
||||
|
||||
fetch(apiUrl + '/api/health', {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
cache: 'no-cache'
|
||||
})
|
||||
.then(response => {
|
||||
checkingDiv.classList.add('hidden');
|
||||
if (! response.ok) {
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
checkingDiv.classList.add('hidden');
|
||||
errorDiv.classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -18,104 +18,50 @@
|
||||
<p class="text-secondary">Enter password to access the system</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="space-y-6">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('login_post', return_url=request.args.get('return_url', '')) }}" class="space-y-6">
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-label mb-2">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autofocus
|
||||
class="w-full px-4 py-3 border border-divider rounded-lg focus:outline-none focus:ring-2 input-focus-ring focus:border-transparent"
|
||||
placeholder="Enter your password"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div id="error-message" class="hidden bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-5 w-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span id="error-text">Invalid password</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-primary w-full font-semibold py-3 px-4 transform hover:scale-105"
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-card">
|
||||
<p class="text-sm text-secondary text-center">
|
||||
<strong>Note:</strong> Password is stored locally in your browser
|
||||
Login is handled with a secure session cookie. No password is stored in the browser.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('loginForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = document.getElementById('password').value;
|
||||
const errorDiv = document.getElementById('error-message');
|
||||
const errorText = document.getElementById('error-text');
|
||||
|
||||
// Hide error message
|
||||
errorDiv.classList.add('hidden');
|
||||
|
||||
try {
|
||||
// Test the password by making a simple API call
|
||||
const response = await fetch('{{ API_BASE_URL }}/api/health', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Password': password
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Store password in localStorage
|
||||
localStorage.setItem('detonator_auth_password', password);
|
||||
|
||||
// Redirect to home page or return URL
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return') || '/';
|
||||
window.location.href = returnUrl;
|
||||
} else {
|
||||
// Show error
|
||||
errorText.textContent = 'Invalid password. Please try again.';
|
||||
errorDiv.classList.remove('hidden');
|
||||
document.getElementById('password').value = '';
|
||||
document.getElementById('password').focus();
|
||||
}
|
||||
} catch (error) {
|
||||
errorText.textContent = 'Connection error. Please check if the API server is running.';
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Check if already logged in
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
const password = localStorage.getItem('detonator_auth_password');
|
||||
if (password) {
|
||||
// Already have a password, verify it and redirect
|
||||
fetch('{{ API_BASE_URL }}/api/health', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Auth-Password': password
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return') || '/';
|
||||
window.location.href = returnUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,21 +18,16 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<h1 class="text-2xl font-bold text-heading mb-2">Logged Out</h1>
|
||||
<p class="text-secondary mb-6">You have been successfully logged out.</p>
|
||||
|
||||
<a
|
||||
href="/login"
|
||||
|
||||
<a
|
||||
href="/login"
|
||||
class="btn-info inline-block font-semibold py-3 px-6"
|
||||
>
|
||||
Login Again
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Clear password from localStorage
|
||||
localStorage.removeItem('detonator_auth_password');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -396,11 +396,11 @@
|
||||
}
|
||||
|
||||
// Check authentication and set runtime field accordingly
|
||||
const password = localStorage.getItem('auth_password');
|
||||
const isAuth = {{ IS_AUTHENTICATED|lower }};
|
||||
const runtimeField = document.getElementById('runtime');
|
||||
const runtimeDescription = document.getElementById('runtime_description');
|
||||
|
||||
if (!password) {
|
||||
|
||||
if (!isAuth) {
|
||||
// User is not authenticated - set runtime to 12 and make read-only
|
||||
runtimeField.value = 12;
|
||||
runtimeField.readOnly = true;
|
||||
|
||||
@@ -140,10 +140,7 @@
|
||||
|
||||
function downloadFile(fileId, filename) {
|
||||
fetch('{{ API_BASE_URL }}/api/files/' + fileId + '/download', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Auth-Password': localStorage.getItem('authToken') || ''
|
||||
}
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -58,8 +58,8 @@ document.head.appendChild(style);
|
||||
|
||||
function deleteProfile(profileId, profileName) {
|
||||
if (confirm(`Are you sure you want to delete profile "${profileName}"?`)) {
|
||||
fetch(`{{ API_BASE_URL }}/api/profiles/${profileId}`, {
|
||||
method: 'DELETE'
|
||||
fetch(`/api/profiles/${profileId}/delete`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -89,7 +89,7 @@ function deleteProfile(profileId, profileName) {
|
||||
|
||||
function releaseLock(profileId, profileName) {
|
||||
if (confirm(`Are you sure you want to release the lock for profile "${profileName}"?`)) {
|
||||
fetch(`{{ API_BASE_URL }}/api/profiles/${profileId}/release_lock`, {
|
||||
fetch(`/api/profiles/${profileId}/release_lock`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -120,7 +120,7 @@ function releaseLock(profileId, profileName) {
|
||||
|
||||
function rebootProfile(profileId, profileName) {
|
||||
if (confirm(`Are you sure you want to reboot the VM for profile "${profileName}"?`)) {
|
||||
fetch(`{{ API_BASE_URL }}/api/profiles/${profileId}/reboot`, {
|
||||
fetch(`/api/profiles/${profileId}/reboot`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
@@ -160,7 +160,7 @@ function revertProfile(profileId, profileName) {
|
||||
processingMsg.innerHTML = '<div class="flex items-center"><div class="inline-block animate-spin rounded-full h-4 w-4 border-b-2 spinner-border mr-2"></div>Reverting VM... This may take a minute.</div>';
|
||||
document.body.appendChild(processingMsg);
|
||||
|
||||
fetch(`{{ API_BASE_URL }}/api/profiles/${profileId}/revert`, {
|
||||
fetch(`/api/profiles/${profileId}/revert`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -196,8 +196,8 @@
|
||||
|
||||
function deleteSubmission(submissionId) {
|
||||
if (confirm('Are you sure you want to delete this submission? This action cannot be undone.')) {
|
||||
fetch('{{ API_BASE_URL }}/api/submissions/' + submissionId, {
|
||||
method: 'DELETE'
|
||||
fetch('/api/submissions/' + submissionId + '/delete', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
|
||||
Reference in New Issue
Block a user