COORs tool to check status

This commit is contained in:
seniorswe
2025-09-26 22:52:46 -04:00
parent 9fdd760095
commit 679148c136
7 changed files with 365 additions and 0 deletions

View File

@@ -23,6 +23,8 @@ Doorman supports user management, authentication, authorizaiton, dynamic routing
## Coming Enhancements
Doorman will soon support transformation, field encryption, and orchestration. More features to be announced.
- CORS checker: No preflight/actual request simulator to validate allowed origins/methods/headers.
## Request Validation
Doorman can validate request payloads at the endpoint level before proxying to your upstream service.

View File

@@ -68,6 +68,7 @@ Core variables:
- Check readiness endpoint; verify Redis/Mongo health; inspect logs via `/platform/logging/logs`.
- CORS failures:
- Verify ALLOWED_ORIGINS and CORS_STRICT settings; avoid `*` with credentials.
- Use Tools → CORS Checker (or POST `/platform/tools/cors/check`) to simulate preflight/actual decisions and view effective headers.
- CSRF errors:
- Ensure clients set `X-CSRF-Token` header to value of `csrf_token` cookie when HTTPS_ENABLED=true.

View File

@@ -41,6 +41,7 @@ from routes.credit_routes import credit_router
from routes.demo_routes import demo_router
from routes.monitor_routes import monitor_router
from routes.config_routes import config_router
from routes.tools_routes import tools_router
from utils.security_settings_util import load_settings, start_auto_save_task, stop_auto_save_task, get_cached_settings
from utils.memory_dump_util import dump_memory_to_file, restore_memory_from_file, find_latest_dump_path
from utils.metrics_util import metrics_store
@@ -478,6 +479,7 @@ doorman.include_router(monitor_router, prefix="/platform", tags=["Monitor"])
doorman.include_router(credit_router, prefix="/platform/credit", tags=["Credit"])
doorman.include_router(demo_router, prefix="/platform/demo", tags=["Demo"])
doorman.include_router(config_router, prefix="/platform", tags=["Config"])
doorman.include_router(tools_router, prefix="/platform/tools", tags=["Tools"])
def start():
if os.path.exists(PID_FILE):

View File

@@ -604,3 +604,37 @@
2025-09-26 22:43:45,235 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"okgrpc/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"579593e1-d37f-46d3-aa79-46f2539aa8b1"}
2025-09-26 22:43:45,238 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:okgrpc/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"a47f88e7-a8bf-4ae5-92d0-57d16acae938"}
2025-09-26 22:43:45,596 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"listapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"1f98eb04-b8bc-4677-b769-976c881a1541"}
2025-09-26 22:43:58,238 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"multi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"25a0297e-1196-40ee-8126-81e5c551d7c3"}
2025-09-26 22:43:58,589 - doorman.audit - INFO - {"actor":"admin","action":"security.update_settings","target":"security_settings","status":"success","details":{"enable_auto_save":false,"auto_save_frequency_seconds":900,"dump_path":"generated/memory_dump.bin"},"ip":"127.0.0.1","path":"/platform/security/settings","request_id":"1cb541e4-51f3-4e09-ae15-55288125c505"}
2025-09-26 22:44:11,086 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"jsonplaceholder/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"0c93d81d-4fae-4872-aa8a-eca50b55d57f"}
2025-09-26 22:44:11,088 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:jsonplaceholder/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"d9309c41-1991-4d14-9f7e-5449e231e7bb"}
2025-09-26 22:44:11,090 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"httpbin/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"1ab6399b-4b5d-411d-90d3-a7d3895752af"}
2025-09-26 22:44:11,091 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:httpbin/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"e8f5e71f-d1dc-4573-bcf9-bf9a538f61c7"}
2025-09-26 22:44:11,094 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"gh-pokeapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"c7022024-ca93-4723-95be-ae18bfcabfcc"}
2025-09-26 22:44:11,095 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:gh-pokeapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"6598c074-2498-43fb-8701-a7aabb0d261a"}
2025-09-26 22:44:11,096 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"gh-swapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"06b3d5b0-a605-49b5-a1c9-530d9c18f1b9"}
2025-09-26 22:44:11,097 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:gh-swapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"737bb270-37c7-4668-87c2-2b8fceb49926"}
2025-09-26 22:44:11,098 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"soap-number/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"4a901cd2-f5bf-4864-b5f0-474fff037b46"}
2025-09-26 22:44:11,099 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:soap-number/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"dd27c41b-e4b7-4899-b73c-97dc624694a1"}
2025-09-26 22:44:11,100 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"soap-tempconvert/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"ed4c93fe-47db-4ec5-98c3-f9124651df6d"}
2025-09-26 22:44:11,101 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:soap-tempconvert/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"d4ef6b4c-cfca-4063-8552-b73c07a90496"}
2025-09-26 22:44:11,102 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"grpc-echo/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"20ed5799-25cc-4ec4-8e07-9cd42073cd34"}
2025-09-26 22:44:11,103 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:grpc-echo/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"a83c762d-2e8b-468d-aece-d83bf9d5a660"}
2025-09-26 22:44:11,104 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"grpc-calc/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"e93ff210-b26b-49ef-ac20-41e8519f6031"}
2025-09-26 22:44:11,105 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:grpc-calc/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"f3db3de7-8d4b-4ad2-a89f-7458ae55aa4d"}
2025-09-26 22:44:14,149 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"orders/v1","status":200,"details":{"message":"API already exists"},"ip":"127.0.0.1","path":"/platform/api","request_id":"b751c3d3-eb7c-409b-b1de-c89c9203e6f7"}
2025-09-26 22:44:14,150 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:orders/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"390265f6-d5c3-4a84-bc75-0836cdff5822"}
2025-09-26 22:44:14,152 - doorman.audit - INFO - {"actor":"admin","action":"subscription.unsubscribe","target":"admin:orders/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/unsubscribe","request_id":"3b2a7128-4f46-4559-b964-903ffa9aafbd"}
2025-09-26 22:44:14,324 - doorman.audit - INFO - {"actor":"admin","action":"credit_def.create","target":"ai-group","status":201,"details":null,"ip":"127.0.0.1","path":"/platform/credit","request_id":"e21ae325-bec0-4efe-8cf5-ab995d003e32"}
2025-09-26 22:44:14,325 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"aiapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"ceb5d2c5-9dd0-4dad-aab7-3b15128d6f9d"}
2025-09-26 22:44:14,327 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:aiapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"661ce5a8-ae13-4d96-bbc5-70e28c86e718"}
2025-09-26 22:44:14,328 - doorman.audit - INFO - {"actor":"admin","action":"user_credits.save","target":"admin","status":200,"details":{"groups":["ai-group"]},"ip":"127.0.0.1","path":"/platform/credit/admin","request_id":"8156e1a8-5f1d-4e97-8052-fe8ac23b1a84"}
2025-09-26 22:44:14,688 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"hdr/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"fb56f7ed-7ea1-49d1-a6c9-51d8a2eb2155"}
2025-09-26 22:44:14,690 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:hdr/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"06097aeb-76ef-4235-9113-30a32105b605"}
2025-09-26 22:44:14,862 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"ratel/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"a65e267d-e370-4428-ad41-faca2027807e"}
2025-09-26 22:44:14,864 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:ratel/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"cc21da61-3709-4215-84c6-eddaac7354b0"}
2025-09-26 22:44:15,209 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"metapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"a2dc1f64-0b6e-46f0-8f8d-267ce6fc42a2"}
2025-09-26 22:44:15,212 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:metapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"77422f61-6e39-459a-95f5-e24924947183"}
2025-09-26 22:44:15,591 - doorman.audit - INFO - {"actor":"admin","action":"api.create","target":"subapi/v1","status":201,"details":{"message":"API created successfully"},"ip":"127.0.0.1","path":"/platform/api","request_id":"60450202-7495-4ee3-b134-f92ab3652cdf"}
2025-09-26 22:44:15,592 - doorman.audit - INFO - {"actor":"admin","action":"subscription.subscribe","target":"admin:subapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/subscribe","request_id":"5162fa36-d731-4718-972a-33ffec36cec2"}
2025-09-26 22:44:15,594 - doorman.audit - INFO - {"actor":"admin","action":"subscription.unsubscribe","target":"admin:subapi/v1","status":200,"details":null,"ip":"127.0.0.1","path":"/platform/subscription/unsubscribe","request_id":"06cd1e36-f29a-4572-b249-6f453ae38993"}

View File

@@ -0,0 +1,171 @@
"""
Tools and diagnostics routes (e.g., CORS checker).
"""
from fastapi import APIRouter, Request
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import os
import uuid
import time
import logging
from models.response_model import ResponseModel
from utils.response_util import process_response
from utils.auth_util import auth_required
from utils.role_util import platform_role_required_bool
tools_router = APIRouter()
logger = logging.getLogger("doorman.gateway")
class CorsCheckRequest(BaseModel):
origin: str = Field(..., description="Origin to evaluate, e.g., https://localhost:3000")
method: str = Field(..., description="Intended request method, e.g., GET/POST/PUT")
request_headers: Optional[List[str]] = Field(default=None, description="Requested headers from Access-Control-Request-Headers")
with_credentials: Optional[bool] = Field(default=None, description="Whether credentials will be sent; defaults to ALLOW_CREDENTIALS env if omitted")
def _compute_cors_config() -> Dict[str, Any]:
origins_env = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000")
origins = [o.strip() for o in origins_env.split(",") if o.strip()]
credentials = os.getenv("ALLOW_CREDENTIALS", "true").lower() == "true"
methods_env = os.getenv("ALLOW_METHODS", "GET,POST,PUT,DELETE,OPTIONS,PATCH,HEAD")
methods = [m.strip().upper() for m in methods_env.split(",") if m.strip()]
if "OPTIONS" not in methods:
methods.append("OPTIONS")
raw_headers = [h.strip() for h in os.getenv("ALLOW_HEADERS", "*").split(",") if h.strip()]
# Mirror doorman.py behavior: avoid wildcard for credentialed requests
if any(h == "*" for h in raw_headers):
headers = ["Accept", "Content-Type", "X-CSRF-Token", "Authorization"]
else:
headers = raw_headers
# Safe origins logic (mirrors doorman.py)
cors_strict = os.getenv("CORS_STRICT", "false").lower() == "true"
if credentials and any(o == "*" for o in origins):
safe_origins = ["http://localhost", "http://localhost:3000"]
elif cors_strict:
safe = [o for o in origins if o != "*"]
safe_origins = safe if safe else ["http://localhost", "http://localhost:3000"]
else:
safe_origins = origins
return {
"origins": origins,
"safe_origins": safe_origins,
"credentials": credentials,
"methods": methods,
"headers": headers,
"cors_strict": cors_strict,
}
@tools_router.post("/cors/check", description="Simulate CORS preflight/actual decisions against current gateway config", response_model=ResponseModel)
async def cors_check(request: Request, body: CorsCheckRequest):
request_id = str(uuid.uuid4())
start_time = time.time() * 1000
try:
payload = await auth_required(request)
username = payload.get("sub")
logger.info(f"{request_id} | Username: {username} | From: {request.client.host}:{request.client.port}")
logger.info(f"{request_id} | Endpoint: {request.method} {str(request.url.path)}")
if not await platform_role_required_bool(username, 'manage_security'):
return process_response(ResponseModel(
status_code=403,
response_headers={"request_id": request_id},
error_code="TLS001",
error_message="You do not have permission to use tools"
).dict(), "rest")
cfg = _compute_cors_config()
origin = (body.origin or '').strip()
method = (body.method or '').strip().upper()
requested_headers = [h.strip() for h in (body.request_headers or []) if h.strip()]
with_credentials = cfg["credentials"] if body.with_credentials is None else bool(body.with_credentials)
# Evaluate origin allowance
origin_allowed = origin in cfg["safe_origins"] or (not cfg["cors_strict"] and "*" in cfg["origins"]) # wildcard only if not strict
method_allowed = method in cfg["methods"]
# Case-insensitive header allowance
allowed_headers_lower = {h.lower() for h in cfg["headers"]}
requested_lower = [h.lower() for h in requested_headers]
headers_not_allowed = [h for h in requested_headers if h.lower() not in allowed_headers_lower]
headers_allowed = len(headers_not_allowed) == 0
preflight_allowed = origin_allowed and method_allowed and headers_allowed
preflight_headers = {
# In practice, middleware may echo requested headers if allowed
"Access-Control-Allow-Origin": origin if origin_allowed else None,
"Access-Control-Allow-Methods": ", ".join(cfg["methods"]),
"Access-Control-Allow-Headers": ", ".join(cfg["headers"]) if requested_headers else ", ".join(cfg["headers"]),
"Access-Control-Allow-Credentials": "true" if with_credentials and cfg["credentials"] else "false",
"Vary": "Origin",
}
actual_allowed = origin_allowed
actual_headers = {
"Access-Control-Allow-Origin": origin if origin_allowed else None,
"Access-Control-Allow-Credentials": "true" if with_credentials and cfg["credentials"] else "false",
"Vary": "Origin",
}
notes: List[str] = []
if cfg["credentials"] and ("*" in cfg["origins"]) and not cfg["cors_strict"]:
notes.append("Wildcard origins with credentials can be rejected by browsers; prefer explicit origins or set CORS_STRICT=true.")
if any(h == "*" for h in os.getenv("ALLOW_HEADERS", "*").split(",")):
notes.append("ALLOW_HEADERS='*' replaced with a conservative default set to satisfy credentialed requests.")
if not origin_allowed:
notes.append("Origin is not allowed based on current configuration.")
if not method_allowed:
notes.append("Requested method is not in ALLOW_METHODS.")
if not headers_allowed and headers_not_allowed:
notes.append(f"Some requested headers are not allowed: {', '.join(headers_not_allowed)}")
response_payload = {
"config": {
"allowed_origins": cfg["origins"],
"effective_allowed_origins": cfg["safe_origins"],
"allow_credentials": cfg["credentials"],
"allow_methods": cfg["methods"],
"allow_headers": cfg["headers"],
"cors_strict": cfg["cors_strict"],
},
"input": {
"origin": origin,
"method": method,
"request_headers": requested_headers,
"with_credentials": with_credentials,
},
"preflight": {
"allowed": preflight_allowed,
"allow_origin": origin_allowed,
"method_allowed": method_allowed,
"headers_allowed": headers_allowed,
"not_allowed_headers": headers_not_allowed,
"response_headers": preflight_headers,
},
"actual": {
"allowed": actual_allowed,
"response_headers": actual_headers,
},
"notes": notes,
}
return process_response(ResponseModel(
status_code=200,
response_headers={"request_id": request_id},
response=response_payload
).dict(), "rest")
except Exception as e:
logger.critical(f"{request_id} | Unexpected error: {str(e)}", exc_info=True)
return process_response(ResponseModel(
status_code=500,
response_headers={"request_id": request_id},
error_code="TLS999",
error_message="An unexpected error occurred"
).dict(), "rest")
finally:
end_time = time.time() * 1000
logger.info(f"{request_id} | Total time: {str(end_time - start_time)}ms")

View File

@@ -0,0 +1,154 @@
'use client'
import React, { useState } from 'react'
import Layout from '@/components/Layout'
import { SERVER_URL } from '@/utils/config'
import { postJson } from '@/utils/api'
import { ProtectedRoute } from '@/components/ProtectedRoute'
interface CorsResult {
config: {
allowed_origins: string[]
effective_allowed_origins: string[]
allow_credentials: boolean
allow_methods: string[]
allow_headers: string[]
cors_strict: boolean
}
input: {
origin: string
method: string
request_headers: string[]
with_credentials: boolean
}
preflight: {
allowed: boolean
allow_origin: boolean
method_allowed: boolean
headers_allowed: boolean
not_allowed_headers: string[]
response_headers: Record<string, string | null>
}
actual: {
allowed: boolean
response_headers: Record<string, string | null>
}
notes: string[]
}
const ToolsPage = () => {
const [origin, setOrigin] = useState('http://localhost:3000')
const [method, setMethod] = useState('GET')
const [headersText, setHeadersText] = useState('Content-Type, Authorization')
const [withCredentials, setWithCredentials] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<CorsResult | null>(null)
const onCheck = async () => {
try {
setLoading(true)
setError(null)
setResult(null)
const payload = {
origin: origin.trim(),
method: method.trim().toUpperCase(),
request_headers: headersText.split(',').map(h => h.trim()).filter(Boolean),
with_credentials: withCredentials,
}
const data = await postJson<CorsResult>(`${SERVER_URL}/platform/tools/cors/check`, payload)
setResult(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Check failed')
} finally {
setLoading(false)
}
}
return (
<ProtectedRoute>
<Layout>
<div className="space-y-6">
<div>
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white">Tools</h1>
<p className="text-sm text-gray-600 dark:text-gray-300">Diagnostics and helpers for operating your gateway.</p>
</div>
<section className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h2 className="text-lg font-medium text-gray-900 dark:text-white mb-3">CORS Checker</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200">Origin</label>
<input className="mt-1 w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" value={origin} onChange={e => setOrigin(e.target.value)} placeholder="https://app.example.com" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200">Method</label>
<input className="mt-1 w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" value={method} onChange={e => setMethod(e.target.value)} placeholder="GET" />
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-200">Request Headers (comma-separated)</label>
<input className="mt-1 w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white" value={headersText} onChange={e => setHeadersText(e.target.value)} placeholder="Content-Type, Authorization" />
</div>
<div className="flex items-center space-x-2">
<input id="withCreds" type="checkbox" checked={withCredentials} onChange={e => setWithCredentials(e.target.checked)} className="rounded border-gray-300 dark:border-gray-600" />
<label htmlFor="withCreds" className="text-sm text-gray-700 dark:text-gray-200">With Credentials</label>
</div>
</div>
<div className="mt-4">
<button onClick={onCheck} disabled={loading} className="px-4 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700 disabled:opacity-50">{loading ? 'Checking…' : 'Check'}</button>
</div>
{error && <div className="mt-3 text-sm text-red-600">{error}</div>}
{result && (
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-3 rounded bg-gray-50 dark:bg-gray-900">
<h3 className="font-medium text-gray-900 dark:text-white">Preflight</h3>
<div className="text-sm text-gray-700 dark:text-gray-200 mt-1">
<div>Allowed: <span className={result.preflight.allowed ? 'text-green-600' : 'text-red-600'}>{String(result.preflight.allowed)}</span></div>
<div>Origin Allowed: {String(result.preflight.allow_origin)}</div>
<div>Method Allowed: {String(result.preflight.method_allowed)}</div>
<div>Headers Allowed: {String(result.preflight.headers_allowed)}</div>
{result.preflight.not_allowed_headers?.length > 0 && (
<div>Not Allowed Headers: {result.preflight.not_allowed_headers.join(', ')}</div>
)}
<div className="mt-2">
<div className="font-medium">Response Headers</div>
<pre className="text-xs whitespace-pre-wrap">{JSON.stringify(result.preflight.response_headers, null, 2)}</pre>
</div>
</div>
</div>
<div className="p-3 rounded bg-gray-50 dark:bg-gray-900">
<h3 className="font-medium text-gray-900 dark:text-white">Actual Request</h3>
<div className="text-sm text-gray-700 dark:text-gray-200 mt-1">
<div>Allowed: <span className={result.actual.allowed ? 'text-green-600' : 'text-red-600'}>{String(result.actual.allowed)}</span></div>
<div className="mt-2">
<div className="font-medium">Response Headers</div>
<pre className="text-xs whitespace-pre-wrap">{JSON.stringify(result.actual.response_headers, null, 2)}</pre>
</div>
</div>
</div>
<div className="md:col-span-2 p-3 rounded bg-gray-50 dark:bg-gray-900">
<h3 className="font-medium text-gray-900 dark:text-white">Effective Config</h3>
<pre className="text-xs text-gray-800 dark:text-gray-200">{JSON.stringify(result.config, null, 2)}</pre>
{result.notes?.length > 0 && (
<div className="mt-2 text-xs text-gray-700 dark:text-gray-300">
<div className="font-medium">Notes</div>
<ul className="list-disc ml-5">
{result.notes.map((n, i) => <li key={i}>{n}</li>)}
</ul>
</div>
)}
</div>
</div>
)}
</section>
</div>
</Layout>
</ProtectedRoute>
)
}
export default ToolsPage

View File

@@ -32,6 +32,7 @@ const menuItems: MenuItem[] = [
{ label: 'Subscriptions', href: '/authorizations', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z', permission: 'manage_subscriptions' },
{ label: 'Auth Control', href: '/auth-admin', icon: 'M5 13l4 4L19 7', permission: 'manage_auth' },
{ label: 'Security', href: '/security', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z', permission: 'manage_security' },
{ label: 'Tools', href: '/tools', icon: 'M12 8v8m-4-4h8M5 3a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2V8.414A2 2 0 0019.586 7L15 2.414A2 2 0 0013.586 2H5z', permission: 'manage_security' },
{ label: 'Import/Export', href: '/import-export', icon: 'M4 4v6h6M20 20v-6h-6M4 10l6-6m4 12l6 6', permission: 'manage_gateway' },
{ label: 'Settings', href: '/settings', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' }
]