Add Apprise alerting capabilities (backend)

- Add name + apprise_url columns to NotificationConfig model
- Add AdminNotificationConfig model for system-wide alert channels
- Create NotificationService with send_user_notification,
  send_admin_notification, and test_notification helpers
- Replace stub notifications.py with full CRUD + /test endpoint
- Add admin notification config CRUD + /test to admin.py
- Update NotificationConfig schemas: name required, apprise_url
  optional, config optional (default {})
- Add AdminNotificationConfig* schemas
- Integrate send_user_notification into process_mail_account task
  for Gmail revocation, forwarding failures, and exceptions
- Update CHANGELOG.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-26 18:06:05 +00:00
parent a6c95bef4d
commit bbd2febfc1
7 changed files with 484 additions and 4 deletions
+93 -3
View File
@@ -1,7 +1,7 @@
"""Notification configuration endpoints"""
from typing import List
from fastapi import APIRouter, Depends, status
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
@@ -10,8 +10,12 @@ from app.core.deps import get_current_active_user
from app.models.database_models import User, NotificationConfig
from app.models.schemas import (
NotificationConfigCreate,
NotificationConfigUpdate,
NotificationConfigResponse,
NotificationTestRequest,
NotificationTestResponse,
)
from app.services.notification_service import test_notification
router = APIRouter()
@@ -24,7 +28,7 @@ async def create_notification_config(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Create notification configuration"""
"""Create a new notification configuration"""
config = NotificationConfig(user_id=current_user.id, **config_in.dict())
db.add(config)
await db.commit()
@@ -37,8 +41,94 @@ async def list_notification_configs(
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""List all notification configurations"""
"""List all notification configurations for the current user"""
result = await db.execute(
select(NotificationConfig).where(NotificationConfig.user_id == current_user.id)
)
return result.scalars().all()
@router.get("/{config_id}", response_model=NotificationConfigResponse)
async def get_notification_config(
config_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Get a specific notification configuration"""
result = await db.execute(
select(NotificationConfig).where(
NotificationConfig.id == config_id,
NotificationConfig.user_id == current_user.id,
)
)
config = result.scalar_one_or_none()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification config not found",
)
return config
@router.put("/{config_id}", response_model=NotificationConfigResponse)
async def update_notification_config(
config_id: int,
config_in: NotificationConfigUpdate,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Update a notification configuration"""
result = await db.execute(
select(NotificationConfig).where(
NotificationConfig.id == config_id,
NotificationConfig.user_id == current_user.id,
)
)
config = result.scalar_one_or_none()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification config not found",
)
update_data = config_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(config, field, value)
await db.commit()
await db.refresh(config)
return config
@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_notification_config(
config_id: int,
current_user: User = Depends(get_current_active_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a notification configuration"""
result = await db.execute(
select(NotificationConfig).where(
NotificationConfig.id == config_id,
NotificationConfig.user_id == current_user.id,
)
)
config = result.scalar_one_or_none()
if not config:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Notification config not found",
)
await db.delete(config)
await db.commit()
@router.post("/test", response_model=NotificationTestResponse)
async def test_notification_config(
request: NotificationTestRequest,
current_user: User = Depends(get_current_active_user),
):
"""Test a notification channel by sending a test message"""
success, message = await test_notification(request.apprise_url)
return NotificationTestResponse(success=success, message=message)