fix(ui): update domain check and unblock functionality with improved error handling and API endpoint adjustments

This commit is contained in:
pacnpal
2025-01-28 16:43:34 +00:00
parent 540ab1d056
commit f4b28cef51
2 changed files with 97 additions and 161 deletions

View File

@@ -22,7 +22,7 @@ from pydantic import BaseModel
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Initialize rate limiter # Initialize FastAPI app
app = FastAPI( app = FastAPI(
title="SimpleGuardHome", title="SimpleGuardHome",
description="AdGuard Home REST API interface", description="AdGuard Home REST API interface",
@@ -46,29 +46,11 @@ app.add_middleware(
templates_path = Path(__file__).parent / "templates" templates_path = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_path)) templates = Jinja2Templates(directory=str(templates_path))
# Response models matching AdGuard spec # Request/Response Models
class HealthResponse(BaseModel): class DomainRequest(BaseModel):
"""Health check response model.""" name: str
status: str
adguard_connection: str
filtering_enabled: bool = False
error: str = None
class DomainResponse(BaseModel):
"""Domain check response model matching AdGuard spec."""
success: bool
domain: str
filtered: bool = False
reason: str = None
rule: str = None
filter_list_id: int = None
service_name: str = None
cname: str = None
ip_addrs: list[str] = None
message: str = None
class ErrorResponse(BaseModel): class ErrorResponse(BaseModel):
"""Error response model matching AdGuard spec."""
message: str message: str
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
@@ -80,43 +62,83 @@ async def home(request: Request):
) )
@app.get( @app.get(
"/control/status", "/control/filtering/check_host",
response_model=HealthResponse, response_model=DomainCheckResult,
responses={ responses={
200: {"description": "OK"}, 200: {"description": "OK"},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}, 400: {"description": "Bad Request", "model": ErrorResponse},
500: {"description": "Internal server error", "model": ErrorResponse} 503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
}, },
tags=["health"] tags=["filtering"]
) )
async def health_check() -> Dict: async def check_domain(name: str) -> Dict:
"""Check the health of the application and AdGuard Home connection.""" """Check if a domain is blocked by AdGuard Home using AdGuard spec."""
if not name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Domain name is required"
)
logger.info(f"Checking domain: {name}")
try: try:
async with adguard.AdGuardClient() as client: async with adguard.AdGuardClient() as client:
status = await client.get_filter_status() result = await client.check_domain(name)
return { logger.info(f"Domain check result: {result}")
"status": "healthy", return result
"adguard_connection": "connected",
"filtering_enabled": status.enabled if status else False
}
except AdGuardConnectionError:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={
"status": "degraded",
"adguard_connection": "failed",
"error": "Could not connect to AdGuard Home"
}
)
except Exception as e: except Exception as e:
logger.error(f"Health check failed: {str(e)}") logger.error(f"Error checking domain {name}: {str(e)}")
return JSONResponse( raise
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={ @app.post(
"status": "error", "/control/filtering/whitelist/add",
"error": "An internal error has occurred. Please try again later." response_model=Dict,
} responses={
200: {"description": "OK"},
400: {"description": "Bad Request", "model": ErrorResponse},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
},
tags=["filtering"]
)
async def add_to_whitelist(request: DomainRequest) -> Dict:
"""Add a domain to the allowed list using AdGuard spec."""
if not request.name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Domain name is required"
) )
logger.info(f"Adding domain to whitelist: {request.name}")
try:
async with adguard.AdGuardClient() as client:
success = await client.add_allowed_domain(request.name)
if success:
return {"message": f"Successfully whitelisted {request.name}"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to whitelist domain"
)
except Exception as e:
logger.error(f"Error whitelisting domain {request.name}: {str(e)}")
raise
@app.get(
"/control/filtering/status",
response_model=FilterStatus,
responses={
200: {"description": "OK"},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
},
tags=["filtering"]
)
async def get_filtering_status() -> FilterStatus:
"""Get the current filtering status using AdGuard spec."""
try:
async with adguard.AdGuardClient() as client:
return await client.get_filter_status()
except Exception as e:
logger.error(f"Error getting filter status: {str(e)}")
raise
@app.exception_handler(AdGuardError) @app.exception_handler(AdGuardError)
async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse: async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSONResponse:
@@ -133,96 +155,6 @@ async def adguard_exception_handler(request: Request, exc: AdGuardError) -> JSON
content={"message": str(exc)} content={"message": str(exc)}
) )
@app.post(
"/control/filtering/check_host",
response_model=DomainResponse,
responses={
200: {"description": "OK"},
400: {"description": "Bad Request", "model": ErrorResponse},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
},
tags=["filtering"]
)
async def check_domain(domain: str = Form(...)) -> Dict:
"""Check if a domain is blocked by AdGuard Home."""
if not domain:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Domain is required"
)
logger.info(f"Checking domain: {domain}")
try:
async with adguard.AdGuardClient() as client:
result = await client.check_domain(domain)
response = {
"success": True,
"domain": domain,
"filtered": result.reason.startswith("Filtered"),
"reason": result.reason,
"rule": result.rule,
"filter_list_id": result.filter_id,
"service_name": result.service_name,
"cname": result.cname,
"ip_addrs": result.ip_addrs
}
logger.info(f"Domain check result: {response}")
return response
except Exception as e:
logger.error(f"Error checking domain {domain}: {str(e)}")
raise
@app.post(
"/control/filtering/whitelist/add",
response_model=DomainResponse,
responses={
200: {"description": "OK"},
400: {"description": "Bad Request", "model": ErrorResponse},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
},
tags=["filtering"]
)
async def unblock_domain(domain: str = Form(...)) -> Dict:
"""Add a domain to the allowed list."""
if not domain:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Domain is required"
)
logger.info(f"Unblocking domain: {domain}")
try:
async with adguard.AdGuardClient() as client:
await client.add_allowed_domain(domain)
response = {
"success": True,
"domain": domain,
"message": f"Successfully unblocked {domain}"
}
logger.info(f"Domain unblock result: {response}")
return response
except Exception as e:
logger.error(f"Error unblocking domain {domain}: {str(e)}")
raise
@app.get(
"/control/filtering/status",
response_model=FilterStatus,
responses={
200: {"description": "OK"},
503: {"description": "AdGuard Home service unavailable", "model": ErrorResponse}
},
tags=["filtering"]
)
async def get_filtering_status() -> FilterStatus:
"""Get the current filtering status."""
try:
async with adguard.AdGuardClient() as client:
return await client.get_filter_status()
except Exception as e:
logger.error(f"Error getting filter status: {str(e)}")
raise
def start(): def start():
"""Start the application using uvicorn.""" """Start the application using uvicorn."""
import uvicorn import uvicorn

View File

@@ -7,7 +7,7 @@
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script> <script>
async function checkDomain(event) { async function checkDomain(event) {
event.preventDefault(); event.preventDefault(); // Important: prevent default form submission
const domain = document.getElementById('domain').value; const domain = document.getElementById('domain').value;
const resultDiv = document.getElementById('result'); const resultDiv = document.getElementById('result');
const unblockDiv = document.getElementById('unblock-action'); const unblockDiv = document.getElementById('unblock-action');
@@ -18,23 +18,24 @@
submitBtn.disabled = true; submitBtn.disabled = true;
submitBtn.innerHTML = '<span class="inline-flex items-center">Checking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>'; submitBtn.innerHTML = '<span class="inline-flex items-center">Checking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>';
const response = await fetch('/check-domain', { const response = await fetch(`/control/filtering/check_host?name=${encodeURIComponent(domain)}`, {
method: 'POST', method: 'GET',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json',
}, }
body: `domain=${encodeURIComponent(domain)}`
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (response.ok) {
if (data.blocked) { if (data.reason.startsWith('Filtered')) {
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4"> <div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4">
<p class="font-bold">Domain is blocked</p> <p class="font-bold">Domain is blocked</p>
<p class="text-sm"><strong>${domain}</strong> is blocked by rule:</p> <p class="text-sm"><strong>${domain}</strong> is blocked by rule:</p>
<p class="text-sm font-mono bg-red-50 p-2 mt-1 rounded">${data.rule || 'Unknown rule'}</p> <p class="text-sm font-mono bg-red-50 p-2 mt-1 rounded">${data.rule || 'No rule specified'}</p>
${data.filter_list ? `<p class="text-sm mt-2">Filter List: ${data.filter_list}</p>` : ''} ${data.filter_id ? `<p class="text-sm mt-2">Filter ID: ${data.filter_id}</p>` : ''}
${data.service_name ? `<p class="text-sm mt-2">Service: ${data.service_name}</p>` : ''}
</div>`; </div>`;
unblockDiv.innerHTML = ` unblockDiv.innerHTML = `
<button onclick="unblockDomain('${domain}')" <button onclick="unblockDomain('${domain}')"
@@ -46,6 +47,7 @@
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4"> <div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4">
<p class="font-bold">Domain is not blocked</p> <p class="font-bold">Domain is not blocked</p>
<p class="text-sm"><strong>${domain}</strong> is allowed</p> <p class="text-sm"><strong>${domain}</strong> is allowed</p>
<p class="text-xs mt-2">Status: ${data.reason}</p>
</div>`; </div>`;
unblockDiv.innerHTML = ''; unblockDiv.innerHTML = '';
} }
@@ -53,8 +55,8 @@
// Show error message // Show error message
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"> <div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4">
<p class="font-bold">${data.error}</p> <p class="font-bold">Error checking domain</p>
<p class="text-sm">${data.detail}</p> <p class="text-sm">${data.message || 'Unknown error occurred'}</p>
</div>`; </div>`;
unblockDiv.innerHTML = ''; unblockDiv.innerHTML = '';
} }
@@ -82,27 +84,28 @@
unblockBtn.disabled = true; unblockBtn.disabled = true;
unblockBtn.innerHTML = '<span class="inline-flex items-center">Unblocking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>'; unblockBtn.innerHTML = '<span class="inline-flex items-center">Unblocking... <svg class="animate-spin ml-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg></span>';
const response = await fetch('/unblock-domain', { const response = await fetch('/control/filtering/whitelist/add', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/json',
'Accept': 'application/json'
}, },
body: `domain=${encodeURIComponent(domain)}` body: JSON.stringify({ name: domain })
}); });
const data = await response.json();
if (data.success) { if (response.ok) {
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4"> <div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4">
<p class="font-bold">Success!</p> <p class="font-bold">Success!</p>
<p class="text-sm">${data.message}</p> <p class="text-sm">Domain ${domain} has been added to the whitelist</p>
</div>`; </div>`;
unblockDiv.innerHTML = ''; unblockDiv.innerHTML = '';
} else { } else {
const data = await response.json();
resultDiv.innerHTML = ` resultDiv.innerHTML = `
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4"> <div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4">
<p class="font-bold">${data.error}</p> <p class="font-bold">Error unblocking domain</p>
<p class="text-sm">${data.detail}</p> <p class="text-sm">${data.message || 'Unknown error occurred'}</p>
</div>`; </div>`;
} }
} catch (error) { } catch (error) {
@@ -120,6 +123,7 @@
<h1 class="text-3xl font-bold text-center mb-8 text-gray-800">SimpleGuardHome</h1> <h1 class="text-3xl font-bold text-center mb-8 text-gray-800">SimpleGuardHome</h1>
<div class="bg-white rounded-lg shadow-md p-6"> <div class="bg-white rounded-lg shadow-md p-6">
<!-- Remove form action to prevent default submission -->
<form onsubmit="checkDomain(event)" class="mb-6"> <form onsubmit="checkDomain(event)" class="mb-6">
<div class="mb-4"> <div class="mb-4">
<label for="domain" class="block text-gray-700 text-sm font-bold mb-2"> <label for="domain" class="block text-gray-700 text-sm font-bold mb-2">