feat(deps): add require_verified_email_after_grace guard

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:44:50 -04:00
parent 9ec208f6e7
commit 519c7eb5ce
3 changed files with 141 additions and 2 deletions

View File

@@ -289,3 +289,47 @@ async def require_active_subscription(
)
return sub
_EMAIL_VERIFICATION_ALLOWLIST = {
"/api/v1/auth/me",
"/api/v1/auth/logout",
"/api/v1/auth/email/send-verification",
"/api/v1/auth/email/verify",
"/api/v1/auth/password/change",
"/api/v1/users/me",
"/api/v1/billing/state",
"/api/v1/billing/checkout-session",
"/api/v1/billing/portal-session",
}
VERIFICATION_GRACE_DAYS = 7
async def require_verified_email_after_grace(
request: Request,
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""Enforces 'this user has verified email OR is still in 7-day grace.'
OAuth signups bypass cleanly because /auth/{google,microsoft}/callback
sets users.email_verified_at = now() (provider-attested)."""
from datetime import datetime, timezone, timedelta
if request.url.path in _EMAIL_VERIFICATION_ALLOWLIST:
return
if current_user.email_verified_at is not None:
return
grace_ends = current_user.created_at + timedelta(days=VERIFICATION_GRACE_DAYS)
if datetime.now(timezone.utc) < grace_ends:
return
raise HTTPException(
status_code=403,
detail={
"error": "email_not_verified",
"grace_ended_at": grace_ends.isoformat(),
"resend_url": "/api/v1/auth/email/send-verification",
},
)

View File

@@ -1,6 +1,10 @@
from fastapi import APIRouter, Depends
from app.api.deps import require_tenant_context, require_active_subscription
from app.api.deps import (
require_tenant_context,
require_active_subscription,
require_verified_email_after_grace,
)
from app.api.endpoints import (
admin,
admin_audit,
@@ -112,7 +116,11 @@ api_router.include_router(admin_gallery.router)
# bypass the gate for billing/account admin/auth flows.
# ---------------------------------------------------------------------------
_tenant_deps = [Depends(require_tenant_context)]
_pro_deps = [Depends(require_tenant_context), Depends(require_active_subscription)]
_pro_deps = [
Depends(require_tenant_context),
Depends(require_active_subscription),
Depends(require_verified_email_after_grace),
]
api_router.include_router(trees.router, dependencies=_pro_deps)
api_router.include_router(sidebar.router, dependencies=_tenant_deps)