Add invite request system and Gitea Actions CI/CD pipeline
Some checks failed
Build & Deploy Staging / build (release) Waiting to run
Build & Deploy Staging / deploy (release) Has been cancelled

Invite request feature:
- Public form to request an invite when INVITE_REQUEST_ENABLED=true
- Stores requests in new invite_requests DB table
- Emails admins on new request, emails requester on approve/deny
- Admin panel tab to review, approve, and deny requests
- Approval auto-creates invite code and sends signup link

CI/CD pipeline:
- Build & push Docker image to Gitea registry on release
- Auto-deploy to staging with health check
- Manual workflow_dispatch for production deploys

Also includes client layout/sizing improvements for card grid
and opponent spacing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-04-07 19:38:52 -04:00
parent 0c0588f920
commit ef54ac201a
16 changed files with 1003 additions and 50 deletions

View File

@@ -418,3 +418,76 @@ async def revoke_invite_code(
if not success:
raise HTTPException(status_code=404, detail="Invite code not found")
return {"message": "Invite code revoked successfully"}
# =============================================================================
# Invite Request Endpoints
# =============================================================================
@router.get("/invite-requests")
async def list_invite_requests(
status: Optional[str] = None,
admin: User = Depends(require_admin_v2),
service: AdminService = Depends(get_admin_service_dep),
):
"""List invite requests, optionally filtered by status (pending, approved, denied)."""
requests = await service.get_invite_requests(status=status)
return {"requests": [r.to_dict() for r in requests]}
@router.post("/invite-requests/{request_id}/approve")
async def approve_invite_request(
request_id: int,
request: Request,
admin: User = Depends(require_admin_v2),
service: AdminService = Depends(get_admin_service_dep),
):
"""Approve an invite request — creates a code and emails the requester."""
code = await service.approve_invite_request(
request_id=request_id,
admin_id=admin.id,
ip_address=get_client_ip(request),
)
if not code:
raise HTTPException(status_code=404, detail="Request not found or already handled")
# Get the request details to send the approval email
requests = await service.get_invite_requests()
req = next((r for r in requests if r.id == request_id), None)
if req:
from services.email_service import get_email_service
email_service = get_email_service()
await email_service.send_invite_approved_email(
to=req.email,
name=req.name,
invite_code=code,
)
return {"code": code, "message": "Request approved and invite sent"}
@router.post("/invite-requests/{request_id}/deny")
async def deny_invite_request(
request_id: int,
request: Request,
admin: User = Depends(require_admin_v2),
service: AdminService = Depends(get_admin_service_dep),
):
"""Deny an invite request — optionally emails the requester."""
result = await service.deny_invite_request(
request_id=request_id,
admin_id=admin.id,
ip_address=get_client_ip(request),
)
if not result:
raise HTTPException(status_code=404, detail="Request not found or already handled")
from services.email_service import get_email_service
email_service = get_email_service()
await email_service.send_invite_denied_email(
to=result["email"],
name=result["name"],
)
return {"message": "Request denied"}

View File

@@ -75,6 +75,13 @@ class UpdatePreferencesRequest(BaseModel):
preferences: dict
class InviteRequestBody(BaseModel):
"""Invite request body."""
name: str
email: str
message: Optional[str] = None
class ConvertGuestRequest(BaseModel):
"""Convert guest to user request."""
guest_id: str
@@ -332,6 +339,7 @@ async def signup_info():
return {
"invite_required": invite_required,
"invite_request_enabled": config.INVITE_REQUEST_ENABLED,
"open_signups_enabled": open_signups_enabled,
"daily_limit": config.DAILY_OPEN_SIGNUPS if not unlimited else None,
"remaining_today": remaining,
@@ -339,6 +347,55 @@ async def signup_info():
}
@router.post("/request-invite")
async def request_invite(
request_body: InviteRequestBody,
request: Request,
):
"""
Public endpoint: submit a request for an invite code.
Stores the request in the database and notifies admins via email.
"""
if not config.INVITE_REQUEST_ENABLED:
raise HTTPException(status_code=404, detail="Invite requests are not enabled")
if not _admin_service:
raise HTTPException(status_code=503, detail="Service not initialized")
name = request_body.name.strip()
email = request_body.email.strip().lower()
message = request_body.message.strip() if request_body.message else None
if not name or len(name) > 100:
raise HTTPException(status_code=400, detail="Name is required (max 100 characters)")
if not email or "@" not in email:
raise HTTPException(status_code=400, detail="Valid email is required")
client_ip = get_client_ip(request)
request_id = await _admin_service.create_invite_request(
name=name,
email=email,
message=message,
ip_address=client_ip,
)
# Notify admin emails
if config.ADMIN_EMAILS:
from services.email_service import get_email_service
email_service = get_email_service()
for admin_email in config.ADMIN_EMAILS:
await email_service.send_invite_request_admin_notification(
to=admin_email,
requester_name=name,
requester_email=email,
message=message or "",
)
return {"status": "ok", "message": "Your request has been submitted. We'll be in touch!"}
@router.post("/verify-email")
async def verify_email(
request_body: VerifyEmailRequest,