mirror of
https://github.com/pacnpal/simpleguardhome.git
synced 2025-12-20 12:31:16 -05:00
fix(ui): update domain check and unblock functionality with improved error handling and API endpoint adjustments
This commit is contained in:
@@ -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",
|
except Exception as e:
|
||||||
"filtering_enabled": status.enabled if status else False
|
logger.error(f"Error checking domain {name}: {str(e)}")
|
||||||
}
|
raise
|
||||||
except AdGuardConnectionError:
|
|
||||||
return JSONResponse(
|
@app.post(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
"/control/filtering/whitelist/add",
|
||||||
content={
|
response_model=Dict,
|
||||||
"status": "degraded",
|
responses={
|
||||||
"adguard_connection": "failed",
|
200: {"description": "OK"},
|
||||||
"error": "Could not connect to AdGuard Home"
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Health check failed: {str(e)}")
|
logger.error(f"Error whitelisting domain {request.name}: {str(e)}")
|
||||||
return JSONResponse(
|
raise
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
content={
|
@app.get(
|
||||||
"status": "error",
|
"/control/filtering/status",
|
||||||
"error": "An internal error has occurred. Please try again later."
|
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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user