First Build alpha 0.1
This commit is contained in:
0
app/routers/__init__.py
Normal file
0
app/routers/__init__.py
Normal file
97
app/routers/auth.py
Normal file
97
app/routers/auth.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Authentication API endpoints — login, logout, current user, password change."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import create_access_token, get_current_user
|
||||
from app.models import User
|
||||
from app.utils.security import hash_password, verify_password
|
||||
from app.utils.validators import ChangePasswordRequest, LoginRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(payload: LoginRequest, db: Session = Depends(get_db)):
|
||||
"""Authenticate and return a JWT token.
|
||||
|
||||
Args:
|
||||
payload: Username and password.
|
||||
db: Database session.
|
||||
|
||||
Returns:
|
||||
JSON with ``access_token`` and ``token_type``.
|
||||
"""
|
||||
user = db.query(User).filter(User.username == payload.username).first()
|
||||
if not user or not verify_password(payload.password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid username or password.",
|
||||
)
|
||||
if not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is disabled.",
|
||||
)
|
||||
|
||||
token = create_access_token(user.username)
|
||||
logger.info("User %s logged in.", user.username)
|
||||
return {
|
||||
"access_token": token,
|
||||
"token_type": "bearer",
|
||||
"user": user.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(current_user: User = Depends(get_current_user)):
|
||||
"""Logout (client-side token discard).
|
||||
|
||||
Returns:
|
||||
Confirmation message.
|
||||
"""
|
||||
logger.info("User %s logged out.", current_user.username)
|
||||
return {"message": "Logged out successfully."}
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_me(current_user: User = Depends(get_current_user)):
|
||||
"""Return the current authenticated user's profile.
|
||||
|
||||
Returns:
|
||||
User dict (no password hash).
|
||||
"""
|
||||
return current_user.to_dict()
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(
|
||||
payload: ChangePasswordRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Change the current user's password.
|
||||
|
||||
Args:
|
||||
payload: Current and new password.
|
||||
current_user: Authenticated user.
|
||||
db: Database session.
|
||||
|
||||
Returns:
|
||||
Confirmation message.
|
||||
"""
|
||||
if not verify_password(payload.current_password, current_user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current password is incorrect.",
|
||||
)
|
||||
|
||||
current_user.password_hash = hash_password(payload.new_password)
|
||||
db.commit()
|
||||
logger.info("Password changed for user %s.", current_user.username)
|
||||
return {"message": "Password changed successfully."}
|
||||
231
app/routers/customers.py
Normal file
231
app/routers/customers.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""Customer CRUD API endpoints with automatic deployment on create."""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import Customer, Deployment, DeploymentLog, User
|
||||
from app.services import netbird_service
|
||||
from app.utils.validators import CustomerCreate, CustomerUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("", status_code=status.HTTP_201_CREATED)
|
||||
async def create_customer(
|
||||
payload: CustomerCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Create a new customer and trigger auto-deployment.
|
||||
|
||||
Validates that the subdomain is unique, creates the customer record,
|
||||
and launches deployment in the background.
|
||||
|
||||
Args:
|
||||
payload: Customer creation data.
|
||||
background_tasks: FastAPI background task runner.
|
||||
|
||||
Returns:
|
||||
Created customer dict with deployment status.
|
||||
"""
|
||||
# Check subdomain uniqueness
|
||||
existing = db.query(Customer).filter(Customer.subdomain == payload.subdomain).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Subdomain '{payload.subdomain}' is already in use.",
|
||||
)
|
||||
|
||||
customer = Customer(
|
||||
name=payload.name,
|
||||
company=payload.company,
|
||||
subdomain=payload.subdomain,
|
||||
email=payload.email,
|
||||
max_devices=payload.max_devices,
|
||||
notes=payload.notes,
|
||||
status="deploying",
|
||||
)
|
||||
db.add(customer)
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
logger.info("Customer %d (%s) created by %s.", customer.id, customer.subdomain, current_user.username)
|
||||
|
||||
# Deploy in background
|
||||
result = await netbird_service.deploy_customer(db, customer.id)
|
||||
|
||||
response = customer.to_dict()
|
||||
response["deployment"] = result
|
||||
return response
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_customers(
|
||||
page: int = Query(default=1, ge=1),
|
||||
per_page: int = Query(default=25, ge=1, le=100),
|
||||
search: Optional[str] = Query(default=None),
|
||||
status_filter: Optional[str] = Query(default=None, alias="status"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List customers with pagination, search, and status filter.
|
||||
|
||||
Args:
|
||||
page: Page number (1-indexed).
|
||||
per_page: Items per page.
|
||||
search: Search in name, subdomain, email.
|
||||
status_filter: Filter by status.
|
||||
|
||||
Returns:
|
||||
Paginated customer list with metadata.
|
||||
"""
|
||||
query = db.query(Customer)
|
||||
|
||||
if search:
|
||||
like_term = f"%{search}%"
|
||||
query = query.filter(
|
||||
(Customer.name.ilike(like_term))
|
||||
| (Customer.subdomain.ilike(like_term))
|
||||
| (Customer.email.ilike(like_term))
|
||||
| (Customer.company.ilike(like_term))
|
||||
)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Customer.status == status_filter)
|
||||
|
||||
total = query.count()
|
||||
customers = (
|
||||
query.order_by(Customer.created_at.desc())
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
.all()
|
||||
)
|
||||
|
||||
items = []
|
||||
for c in customers:
|
||||
data = c.to_dict()
|
||||
if c.deployment:
|
||||
data["deployment"] = c.deployment.to_dict()
|
||||
else:
|
||||
data["deployment"] = None
|
||||
items.append(data)
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
"pages": (total + per_page - 1) // per_page if total > 0 else 1,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{customer_id}")
|
||||
async def get_customer(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed customer information including deployment and logs.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Customer dict with deployment info and recent logs.
|
||||
"""
|
||||
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found.",
|
||||
)
|
||||
|
||||
data = customer.to_dict()
|
||||
data["deployment"] = customer.deployment.to_dict() if customer.deployment else None
|
||||
data["logs"] = [
|
||||
log.to_dict()
|
||||
for log in db.query(DeploymentLog)
|
||||
.filter(DeploymentLog.customer_id == customer_id)
|
||||
.order_by(DeploymentLog.created_at.desc())
|
||||
.limit(50)
|
||||
.all()
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
@router.put("/{customer_id}")
|
||||
async def update_customer(
|
||||
customer_id: int,
|
||||
payload: CustomerUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update customer information.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
payload: Fields to update.
|
||||
|
||||
Returns:
|
||||
Updated customer dict.
|
||||
"""
|
||||
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found.",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_none=True)
|
||||
for field, value in update_data.items():
|
||||
if hasattr(customer, field):
|
||||
setattr(customer, field, value)
|
||||
|
||||
customer.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(customer)
|
||||
|
||||
logger.info("Customer %d updated by %s.", customer_id, current_user.username)
|
||||
return customer.to_dict()
|
||||
|
||||
|
||||
@router.delete("/{customer_id}")
|
||||
async def delete_customer(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete a customer and clean up all resources.
|
||||
|
||||
Removes containers, NPM proxy, instance directory, and database records.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Confirmation message.
|
||||
"""
|
||||
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
||||
if not customer:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Customer not found.",
|
||||
)
|
||||
|
||||
# Undeploy first (containers, NPM, files)
|
||||
await netbird_service.undeploy_customer(db, customer_id)
|
||||
|
||||
# Delete customer record (cascades to deployment + logs)
|
||||
db.delete(customer)
|
||||
db.commit()
|
||||
|
||||
logger.info("Customer %d deleted by %s.", customer_id, current_user.username)
|
||||
return {"message": f"Customer {customer_id} deleted successfully."}
|
||||
185
app/routers/deployments.py
Normal file
185
app/routers/deployments.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Deployment management API — start, stop, restart, logs, health for customers."""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import Customer, Deployment, User
|
||||
from app.services import docker_service, netbird_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/{customer_id}/deploy")
|
||||
async def manual_deploy(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Manually trigger deployment for a customer.
|
||||
|
||||
Use this to re-deploy a customer whose previous deployment failed.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Deployment result dict.
|
||||
"""
|
||||
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.")
|
||||
|
||||
# Remove existing deployment if present
|
||||
existing = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
|
||||
if existing:
|
||||
await netbird_service.undeploy_customer(db, customer_id)
|
||||
|
||||
result = await netbird_service.deploy_customer(db, customer_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.get("error", "Deployment failed."),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{customer_id}/start")
|
||||
async def start_customer(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Start containers for a customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Result dict.
|
||||
"""
|
||||
_require_customer(db, customer_id)
|
||||
result = netbird_service.start_customer(db, customer_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.get("error", "Failed to start containers."),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{customer_id}/stop")
|
||||
async def stop_customer(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Stop containers for a customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Result dict.
|
||||
"""
|
||||
_require_customer(db, customer_id)
|
||||
result = netbird_service.stop_customer(db, customer_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.get("error", "Failed to stop containers."),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/{customer_id}/restart")
|
||||
async def restart_customer(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Restart containers for a customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Result dict.
|
||||
"""
|
||||
_require_customer(db, customer_id)
|
||||
result = netbird_service.restart_customer(db, customer_id)
|
||||
if not result.get("success"):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=result.get("error", "Failed to restart containers."),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@router.get("/{customer_id}/logs")
|
||||
async def get_customer_logs(
|
||||
customer_id: int,
|
||||
tail: int = 200,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get container logs for a customer.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
tail: Number of log lines per container.
|
||||
|
||||
Returns:
|
||||
Dict mapping container name to log text.
|
||||
"""
|
||||
_require_customer(db, customer_id)
|
||||
deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
|
||||
if not deployment:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="No deployment found for this customer.",
|
||||
)
|
||||
|
||||
logs = docker_service.get_all_container_logs(deployment.container_prefix, tail=tail)
|
||||
return {"logs": logs}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/health")
|
||||
async def check_customer_health(
|
||||
customer_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Run a health check on a customer's deployment.
|
||||
|
||||
Args:
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Health check results.
|
||||
"""
|
||||
_require_customer(db, customer_id)
|
||||
return netbird_service.get_customer_health(db, customer_id)
|
||||
|
||||
|
||||
def _require_customer(db: Session, customer_id: int) -> Customer:
|
||||
"""Helper to fetch a customer or raise 404.
|
||||
|
||||
Args:
|
||||
db: Database session.
|
||||
customer_id: Customer ID.
|
||||
|
||||
Returns:
|
||||
Customer ORM object.
|
||||
|
||||
Raises:
|
||||
HTTPException: If customer not found.
|
||||
"""
|
||||
customer = db.query(Customer).filter(Customer.id == customer_id).first()
|
||||
if not customer:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Customer not found.")
|
||||
return customer
|
||||
116
app/routers/monitoring.py
Normal file
116
app/routers/monitoring.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Monitoring API — system overview, customer statuses, host resources."""
|
||||
|
||||
import logging
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
import psutil
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import Customer, Deployment, User
|
||||
from app.services import docker_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def system_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
"""System overview with aggregated customer statistics.
|
||||
|
||||
Returns:
|
||||
Counts by status and total customers.
|
||||
"""
|
||||
total = db.query(Customer).count()
|
||||
active = db.query(Customer).filter(Customer.status == "active").count()
|
||||
inactive = db.query(Customer).filter(Customer.status == "inactive").count()
|
||||
deploying = db.query(Customer).filter(Customer.status == "deploying").count()
|
||||
error = db.query(Customer).filter(Customer.status == "error").count()
|
||||
|
||||
return {
|
||||
"total_customers": total,
|
||||
"active": active,
|
||||
"inactive": inactive,
|
||||
"deploying": deploying,
|
||||
"error": error,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/customers")
|
||||
async def all_customers_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get deployment status for every customer.
|
||||
|
||||
Returns:
|
||||
List of dicts with customer info and container statuses.
|
||||
"""
|
||||
customers = (
|
||||
db.query(Customer)
|
||||
.order_by(Customer.id)
|
||||
.all()
|
||||
)
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
for c in customers:
|
||||
entry: dict[str, Any] = {
|
||||
"id": c.id,
|
||||
"name": c.name,
|
||||
"subdomain": c.subdomain,
|
||||
"status": c.status,
|
||||
}
|
||||
if c.deployment:
|
||||
containers = docker_service.get_container_status(c.deployment.container_prefix)
|
||||
entry["deployment_status"] = c.deployment.deployment_status
|
||||
entry["containers"] = containers
|
||||
entry["relay_udp_port"] = c.deployment.relay_udp_port
|
||||
entry["setup_url"] = c.deployment.setup_url
|
||||
else:
|
||||
entry["deployment_status"] = None
|
||||
entry["containers"] = []
|
||||
results.append(entry)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/resources")
|
||||
async def host_resources(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> dict[str, Any]:
|
||||
"""Return host system resource usage.
|
||||
|
||||
Returns:
|
||||
CPU, memory, disk, and network information.
|
||||
"""
|
||||
cpu_percent = psutil.cpu_percent(interval=1)
|
||||
cpu_count = psutil.cpu_count()
|
||||
mem = psutil.virtual_memory()
|
||||
disk = psutil.disk_usage("/")
|
||||
|
||||
return {
|
||||
"hostname": platform.node(),
|
||||
"os": f"{platform.system()} {platform.release()}",
|
||||
"cpu": {
|
||||
"percent": cpu_percent,
|
||||
"count": cpu_count,
|
||||
},
|
||||
"memory": {
|
||||
"total_gb": round(mem.total / (1024 ** 3), 1),
|
||||
"used_gb": round(mem.used / (1024 ** 3), 1),
|
||||
"available_gb": round(mem.available / (1024 ** 3), 1),
|
||||
"percent": mem.percent,
|
||||
},
|
||||
"disk": {
|
||||
"total_gb": round(disk.total / (1024 ** 3), 1),
|
||||
"used_gb": round(disk.used / (1024 ** 3), 1),
|
||||
"free_gb": round(disk.free / (1024 ** 3), 1),
|
||||
"percent": disk.percent,
|
||||
},
|
||||
}
|
||||
113
app/routers/settings.py
Normal file
113
app/routers/settings.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""System configuration API — read/write all settings from the database.
|
||||
|
||||
There is no .env file. Every setting lives in the ``system_config`` table
|
||||
(singleton row with id=1) and is editable via the Web UI settings page.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies import get_current_user
|
||||
from app.models import SystemConfig, User
|
||||
from app.services import npm_service
|
||||
from app.utils.config import get_system_config
|
||||
from app.utils.security import encrypt_value
|
||||
from app.utils.validators import SystemConfigUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/system")
|
||||
async def get_settings(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Return all system configuration values (token masked).
|
||||
|
||||
Returns:
|
||||
System config dict.
|
||||
"""
|
||||
row = db.query(SystemConfig).filter(SystemConfig.id == 1).first()
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="System configuration not initialized. Run install.sh first.",
|
||||
)
|
||||
return row.to_dict()
|
||||
|
||||
|
||||
@router.put("/system")
|
||||
async def update_settings(
|
||||
payload: SystemConfigUpdate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update system configuration values.
|
||||
|
||||
Only provided (non-None) fields are updated. The NPM API token is
|
||||
encrypted before storage.
|
||||
|
||||
Args:
|
||||
payload: Fields to update.
|
||||
|
||||
Returns:
|
||||
Updated system config dict.
|
||||
"""
|
||||
row = db.query(SystemConfig).filter(SystemConfig.id == 1).first()
|
||||
if not row:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="System configuration not initialized.",
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_none=True)
|
||||
|
||||
# Handle NPM token encryption
|
||||
if "npm_api_token" in update_data:
|
||||
raw_token = update_data.pop("npm_api_token")
|
||||
row.npm_api_token_encrypted = encrypt_value(raw_token)
|
||||
|
||||
for field, value in update_data.items():
|
||||
if hasattr(row, field):
|
||||
setattr(row, field, value)
|
||||
|
||||
row.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
|
||||
logger.info("System configuration updated by %s.", current_user.username)
|
||||
return row.to_dict()
|
||||
|
||||
|
||||
@router.get("/test-npm")
|
||||
async def test_npm(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Test connectivity to the Nginx Proxy Manager API.
|
||||
|
||||
Loads the NPM URL and decrypted token from the database and attempts
|
||||
to list proxy hosts.
|
||||
|
||||
Returns:
|
||||
Dict with ``ok`` and ``message``.
|
||||
"""
|
||||
config = get_system_config(db)
|
||||
if not config:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="System configuration not initialized.",
|
||||
)
|
||||
if not config.npm_api_url or not config.npm_api_token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="NPM API URL or token not configured.",
|
||||
)
|
||||
|
||||
result = await npm_service.test_npm_connection(config.npm_api_url, config.npm_api_token)
|
||||
return result
|
||||
Reference in New Issue
Block a user