Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Public unban-request intake + admin resolution. One request per IP per
|
||||
24h (enforced here), notifying every admin account's own email (Resend if
|
||||
configured, else SMTP - same provider selection as the rest of security
|
||||
email, see security_email_service.resolve_provider) and via the existing
|
||||
admin-only Redis log feed (app/core/logging.py), so it surfaces in the
|
||||
Settings page's Logging box too.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import Settings
|
||||
from app.core.errors import ConflictError, NotFoundError, RateLimitedError
|
||||
from app.core.logging import get_logger
|
||||
from app.models.ip_ban import IpBan
|
||||
from app.notifications.base import NotificationMessage
|
||||
from app.repositories.ip_throttle_repository import IpThrottleRepository
|
||||
from app.repositories.unban_request_repository import UnbanRequestRepository
|
||||
from app.repositories.user_repository import UserRepository
|
||||
from app.services.security_email_service import resolve_provider
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
UNBAN_REQUEST_COOLDOWN_HOURS = 24
|
||||
|
||||
|
||||
async def submit_unban_request(
|
||||
db: AsyncSession, settings: Settings, ip_address: str, message: str | None
|
||||
) -> None:
|
||||
repo = UnbanRequestRepository(db)
|
||||
if await repo.within_cooldown(ip_address, UNBAN_REQUEST_COOLDOWN_HOURS):
|
||||
raise RateLimitedError(
|
||||
f"Only one unban request is allowed per {UNBAN_REQUEST_COOLDOWN_HOURS} hours."
|
||||
)
|
||||
|
||||
await repo.create(ip_address, message)
|
||||
await db.commit()
|
||||
|
||||
logger.warning("unban_request_received", ip=ip_address, message=message or "")
|
||||
|
||||
admin_emails = await UserRepository(db).list_admin_emails()
|
||||
if not admin_emails:
|
||||
logger.warning("unban_request_no_admin_to_notify", ip=ip_address)
|
||||
return
|
||||
|
||||
provider = resolve_provider(settings)
|
||||
for admin_email in admin_emails:
|
||||
email_message = NotificationMessage(
|
||||
destination_value=admin_email,
|
||||
subject=f"Unban request from {ip_address}",
|
||||
body_text=f"IP: {ip_address}\n\nMessage:\n{message or '(none)'}",
|
||||
)
|
||||
await provider.send(email_message)
|
||||
|
||||
|
||||
async def list_ip_bans(db: AsyncSession) -> list[IpBan]:
|
||||
return await IpThrottleRepository(db).list_bans()
|
||||
|
||||
|
||||
async def unban_ip(db: AsyncSession, ip_address: str) -> bool:
|
||||
repo = IpThrottleRepository(db)
|
||||
cleared = await repo.clear_ban_and_state(ip_address)
|
||||
await db.commit()
|
||||
return cleared
|
||||
|
||||
|
||||
async def ban_ip(db: AsyncSession, ip_address: str, reason: str = "manual_admin_ban") -> IpBan:
|
||||
"""Admin-initiated ban, bypassing the usual offense-count escalation
|
||||
ladder (ip_throttle_service) entirely - a deliberate manual override,
|
||||
not something the automated abuse-detection path produces."""
|
||||
repo = IpThrottleRepository(db)
|
||||
if await repo.get_ban(ip_address) is not None:
|
||||
raise ConflictError(f"{ip_address} is already banned.")
|
||||
ban = await repo.create_ban(ip_address, reason, datetime.now(UTC))
|
||||
await db.commit()
|
||||
return ban
|
||||
|
||||
|
||||
async def accept_unban_request(db: AsyncSession, request_id: uuid.UUID) -> None:
|
||||
"""Unbans the requester's IP and clears the request from the pending
|
||||
queue - a real pardon (see clear_ban_and_state), not just acknowledging
|
||||
the request was read."""
|
||||
repo = UnbanRequestRepository(db)
|
||||
request = await repo.get(request_id)
|
||||
if request is None:
|
||||
raise NotFoundError("Unban request not found.")
|
||||
await IpThrottleRepository(db).clear_ban_and_state(request.ip_address)
|
||||
await repo.delete(request_id)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def reject_unban_request(db: AsyncSession, request_id: uuid.UUID) -> None:
|
||||
"""Dismisses the request without touching the ban - the IP stays
|
||||
banned."""
|
||||
repo = UnbanRequestRepository(db)
|
||||
if await repo.get(request_id) is None:
|
||||
raise NotFoundError("Unban request not found.")
|
||||
await repo.delete(request_id)
|
||||
await db.commit()
|
||||
Reference in New Issue
Block a user