mirror of
https://github.com/apidoorman/doorman.git
synced 2026-02-11 20:18:35 -06:00
COORs tool to check status
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"}
|
||||
|
||||
171
backend-services/routes/tools_routes.py
Normal file
171
backend-services/routes/tools_routes.py
Normal 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")
|
||||
|
||||
154
web-client/src/app/tools/page.tsx
Normal file
154
web-client/src/app/tools/page.tsx
Normal 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
|
||||
|
||||
@@ -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' }
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user