Implement team join request functionality with views and actions
- Added join_requests.html template for displaying pending join requests. - Created join_team.html template for users to submit join requests. - Implemented request_processed.html template to show the result of join request processing. - Developed authentication utilities for user session management. - Introduced convenience redirects for common URL patterns. - Established team management routes and actions for creating, editing, and joining teams. - Added functionality for approving and denying join requests with email notifications. - Enhanced team views to include user permissions and team member details. - Implemented utility functions for team-related operations such as calculating total points and team rank.
This commit is contained in:
@@ -1 +1,4 @@
|
||||
"""
|
||||
Utility functions for LeagueLedger.
|
||||
"""
|
||||
# Utils package initialization
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
Authentication utilities for LeagueLedger.
|
||||
"""
|
||||
from typing import Optional
|
||||
from fastapi import Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from ..db import get_db
|
||||
from ..models import User, TeamMembership
|
||||
|
||||
async def get_current_user(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
|
||||
"""
|
||||
Get the current authenticated user from the session.
|
||||
|
||||
Args:
|
||||
request: The FastAPI request object
|
||||
db: SQLAlchemy database session
|
||||
|
||||
Returns:
|
||||
User object if authenticated, None otherwise
|
||||
"""
|
||||
user_id = request.session.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
return None
|
||||
|
||||
# Fetch the user from the database
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
# If user doesn't exist in database but has a session, clear the session
|
||||
request.session.clear()
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
async def is_team_captain(
|
||||
team_id: int,
|
||||
user: Optional[User] = None,
|
||||
request: Optional[Request] = None,
|
||||
db: Session = Depends(get_db)
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the current user is a captain of the specified team.
|
||||
|
||||
Args:
|
||||
team_id: ID of the team to check
|
||||
user: Optional pre-loaded user object
|
||||
request: Optional FastAPI request object (used if user is not provided)
|
||||
db: SQLAlchemy database session
|
||||
|
||||
Returns:
|
||||
True if the user is a team captain, False otherwise
|
||||
"""
|
||||
if not user and request:
|
||||
user = await get_current_user(request, db)
|
||||
|
||||
if not user:
|
||||
return False
|
||||
|
||||
# Check if the user is a captain of the team
|
||||
is_captain = db.query(TeamMembership).filter(
|
||||
TeamMembership.team_id == team_id,
|
||||
TeamMembership.user_id == user.id,
|
||||
TeamMembership.role == "captain"
|
||||
).first()
|
||||
|
||||
return bool(is_captain)
|
||||
|
||||
async def is_admin(request: Request, db: Session = Depends(get_db)) -> bool:
|
||||
"""
|
||||
Check if the current user is an admin.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
db: SQLAlchemy database session
|
||||
|
||||
Returns:
|
||||
True if the user is an admin, False otherwise
|
||||
"""
|
||||
user = await get_current_user(request, db)
|
||||
|
||||
if not user:
|
||||
return False
|
||||
|
||||
return user.is_admin
|
||||
|
||||
async def requires_login(request: Request, db: Session = Depends(get_db)) -> Optional[User]:
|
||||
"""
|
||||
Dependency to ensure the user is logged in.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
db: SQLAlchemy database session
|
||||
|
||||
Returns:
|
||||
User object if authenticated, raises HTTPException otherwise
|
||||
"""
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
user = await get_current_user(request, db)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
async def requires_admin(request: Request, db: Session = Depends(get_db)) -> User:
|
||||
"""
|
||||
Dependency to ensure the user is an admin.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
db: SQLAlchemy database session
|
||||
|
||||
Returns:
|
||||
User object if authenticated and is admin, raises HTTPException otherwise
|
||||
"""
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
user = await get_current_user(request, db)
|
||||
|
||||
if not user or not user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
return user
|
||||
+198
-169
@@ -8,192 +8,221 @@ from fastapi import BackgroundTasks
|
||||
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType
|
||||
from pydantic import EmailStr
|
||||
from dotenv import load_dotenv
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import logging
|
||||
|
||||
# Setup logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Load environment variables if not already loaded
|
||||
load_dotenv()
|
||||
|
||||
# Get mail settings from environment variables or use default values
|
||||
MAIL_USERNAME = os.getenv("MAIL_USERNAME", "")
|
||||
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD", "")
|
||||
MAIL_FROM = os.getenv("MAIL_FROM", "noreply@leagueledger.com")
|
||||
MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.example.com")
|
||||
MAIL_PORT = int(os.getenv("MAIL_PORT", "587"))
|
||||
MAIL_FROM_NAME = os.getenv("MAIL_FROM_NAME", "LeagueLedger")
|
||||
MAIL_STARTTLS = os.getenv("MAIL_STARTTLS", "True").lower() == "true"
|
||||
MAIL_SSL_TLS = os.getenv("MAIL_SSL_TLS", "False").lower() == "true"
|
||||
USE_CREDENTIALS = os.getenv("MAIL_USE_CREDENTIALS", "True").lower() == "true"
|
||||
VALIDATE_CERTS = os.getenv("MAIL_VALIDATE_CERTS", "True").lower() == "true"
|
||||
APP_BASE_URL = os.getenv("LEAGUELEDGER_BASE_URL", os.getenv("APP_BASE_URL", "http://localhost:8000"))
|
||||
|
||||
# Configure Jinja2 for email templates
|
||||
template_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates")
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
# Configure email connection
|
||||
mail_config = ConnectionConfig(
|
||||
MAIL_USERNAME=os.getenv("MAIL_USERNAME"),
|
||||
MAIL_PASSWORD=os.getenv("MAIL_PASSWORD"),
|
||||
MAIL_FROM=os.getenv("MAIL_FROM"),
|
||||
MAIL_PORT=int(os.getenv("MAIL_PORT", 587)),
|
||||
MAIL_SERVER=os.getenv("MAIL_SERVER"),
|
||||
MAIL_FROM_NAME=os.getenv("MAIL_FROM_NAME", "LeagueLedger"),
|
||||
MAIL_STARTTLS=os.getenv("MAIL_STARTTLS", "True").lower() in ("true", "1", "t"),
|
||||
MAIL_SSL_TLS=os.getenv("MAIL_SSL_TLS", "False").lower() in ("true", "1", "t"),
|
||||
USE_CREDENTIALS=os.getenv("MAIL_USE_CREDENTIALS", "True").lower() in ("true", "1", "t"),
|
||||
VALIDATE_CERTS=os.getenv("MAIL_VALIDATE_CERTS", "True").lower() in ("true", "1", "t"),
|
||||
TEMPLATE_FOLDER=Path(__file__).parent.parent / 'templates' / 'email',
|
||||
conf = ConnectionConfig(
|
||||
MAIL_USERNAME=MAIL_USERNAME,
|
||||
MAIL_PASSWORD=MAIL_PASSWORD,
|
||||
MAIL_FROM=MAIL_FROM,
|
||||
MAIL_PORT=MAIL_PORT,
|
||||
MAIL_SERVER=MAIL_SERVER,
|
||||
MAIL_FROM_NAME=MAIL_FROM_NAME,
|
||||
MAIL_STARTTLS=MAIL_STARTTLS,
|
||||
MAIL_SSL_TLS=MAIL_SSL_TLS,
|
||||
USE_CREDENTIALS=USE_CREDENTIALS,
|
||||
VALIDATE_CERTS=VALIDATE_CERTS
|
||||
)
|
||||
|
||||
# Create FastMail instance
|
||||
mail = FastMail(mail_config)
|
||||
|
||||
|
||||
async def send_email(
|
||||
recipients: List[EmailStr],
|
||||
email_to: List[EmailStr],
|
||||
subject: str,
|
||||
body: str,
|
||||
template_name: Optional[str] = None,
|
||||
template_body: Optional[Dict[str, Any]] = None,
|
||||
background_tasks: Optional[BackgroundTasks] = None,
|
||||
subtype: MessageType = MessageType.html,
|
||||
cc: Optional[List[EmailStr]] = None,
|
||||
bcc: Optional[List[EmailStr]] = None,
|
||||
attachments: Optional[List] = None,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send an email using FastAPI-Mail
|
||||
|
||||
Args:
|
||||
recipients: List of recipient email addresses
|
||||
subject: Email subject
|
||||
body: Email body content (used if template_name is None)
|
||||
template_name: Optional name of the template file in the TEMPLATE_FOLDER
|
||||
template_body: Optional dictionary of template variables
|
||||
background_tasks: Optional BackgroundTasks for sending email in background
|
||||
subtype: Message type (html or plain)
|
||||
cc: Optional list of CC recipients
|
||||
bcc: Optional list of BCC recipients
|
||||
attachments: Optional list of attachments
|
||||
headers: Optional custom email headers
|
||||
"""
|
||||
# Create message schema with empty lists for optional parameters to prevent validation errors
|
||||
message = MessageSchema(
|
||||
subject=subject,
|
||||
recipients=recipients,
|
||||
body=body if not template_name else None,
|
||||
template_body=template_body,
|
||||
subtype=subtype,
|
||||
cc=cc or [], # Use empty list if None
|
||||
bcc=bcc or [], # Use empty list if None
|
||||
attachments=attachments or [], # Use empty list if None
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Send email
|
||||
html_content: str,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""Generic function to send emails"""
|
||||
try:
|
||||
if background_tasks:
|
||||
if template_name:
|
||||
background_tasks.add_task(mail.send_message, message, template_name=template_name)
|
||||
else:
|
||||
background_tasks.add_task(mail.send_message, message)
|
||||
else:
|
||||
if template_name:
|
||||
await mail.send_message(message, template_name=template_name)
|
||||
else:
|
||||
await mail.send_message(message)
|
||||
message = MessageSchema(
|
||||
subject=subject,
|
||||
recipients=[email_to] if isinstance(email_to, str) else email_to,
|
||||
body=html_content,
|
||||
subtype="html"
|
||||
)
|
||||
|
||||
fm = FastMail(conf)
|
||||
|
||||
# Send email in the background to avoid blocking the main thread
|
||||
background_tasks.add_task(fm.send_message, message)
|
||||
logger.info(f"Email queued for sending to {email_to}")
|
||||
return True
|
||||
except Exception as e:
|
||||
# Log the error but don't crash the application
|
||||
print(f"Error sending email: {str(e)}")
|
||||
# In a production app, you would use a proper logging system
|
||||
|
||||
logger.error(f"Failed to send email: {str(e)}")
|
||||
return False
|
||||
|
||||
async def send_password_reset_email(
|
||||
email: EmailStr,
|
||||
email_to: str,
|
||||
username: str,
|
||||
reset_token: str,
|
||||
background_tasks: Optional[BackgroundTasks] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send password reset email
|
||||
|
||||
Args:
|
||||
email: Recipient email address
|
||||
username: User's username
|
||||
reset_token: Password reset token
|
||||
background_tasks: Optional BackgroundTasks for sending in background
|
||||
"""
|
||||
# Base URL for the application (should be configured in environment vars)
|
||||
base_url = os.getenv("LEAGUELEDGER_BASE_URL", "http://localhost:8000")
|
||||
reset_link = f"{base_url}/auth/reset-password?token={reset_token}"
|
||||
|
||||
# Template data
|
||||
template_data = {
|
||||
"username": username,
|
||||
"reset_link": reset_link,
|
||||
"support_email": os.getenv("MAIL_FROM", "support@leagueledger.net"),
|
||||
"base_url": base_url,
|
||||
}
|
||||
|
||||
# Send email
|
||||
await send_email(
|
||||
recipients=[email],
|
||||
subject="Password Reset - LeagueLedger",
|
||||
body="", # Empty as we're using a template
|
||||
template_name="password_reset.html",
|
||||
template_body=template_data,
|
||||
background_tasks=background_tasks,
|
||||
)
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""Send password reset email with a reset link"""
|
||||
try:
|
||||
# Create reset URL with token
|
||||
reset_url = f"{APP_BASE_URL}/auth/reset-password?token={reset_token}"
|
||||
|
||||
# Get the email template
|
||||
template = env.get_template("email/password_reset.html")
|
||||
|
||||
# Render the HTML content with variables
|
||||
html_content = template.render(
|
||||
username=username,
|
||||
reset_url=reset_url,
|
||||
token=reset_token
|
||||
)
|
||||
|
||||
# Send email
|
||||
subject = "Password Reset Request - LeagueLedger"
|
||||
await send_email(
|
||||
email_to=email_to,
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
logger.info(f"Password reset email sent to {email_to}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send password reset email: {str(e)}")
|
||||
return False
|
||||
|
||||
async def send_team_join_request_notification(
|
||||
captain_email: str,
|
||||
captain_name: str,
|
||||
requester_name: str,
|
||||
team_name: str,
|
||||
message: str,
|
||||
approval_token: str,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""Send email notification to team captain about join request"""
|
||||
try:
|
||||
# Create approval/denial URLs
|
||||
approve_url = f"{APP_BASE_URL}/teams/approve-request/{approval_token}"
|
||||
deny_url = f"{APP_BASE_URL}/teams/deny-request/{approval_token}"
|
||||
|
||||
# Get the email template
|
||||
template = env.get_template("email/team_join_request.html")
|
||||
|
||||
# Render the HTML content with variables
|
||||
html_content = template.render(
|
||||
captain_name=captain_name,
|
||||
requester_name=requester_name,
|
||||
team_name=team_name,
|
||||
message=message,
|
||||
approve_url=approve_url,
|
||||
deny_url=deny_url
|
||||
)
|
||||
|
||||
# Send email
|
||||
subject = f"Team Join Request - {requester_name} wants to join {team_name}"
|
||||
await send_email(
|
||||
email_to=captain_email,
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
logger.info(f"Team join request notification sent to {captain_email}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send team join request notification: {str(e)}")
|
||||
return False
|
||||
|
||||
async def send_verification_email(
|
||||
email: EmailStr,
|
||||
username: str,
|
||||
verification_token: str,
|
||||
background_tasks: Optional[BackgroundTasks] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send email verification link
|
||||
|
||||
Args:
|
||||
email: Recipient email address
|
||||
username: User's username
|
||||
verification_token: Email verification token
|
||||
background_tasks: Optional BackgroundTasks for sending in background
|
||||
"""
|
||||
# Base URL for the application
|
||||
base_url = os.getenv("LEAGUELEDGER_BASE_URL", "http://localhost:8000")
|
||||
verification_link = f"{base_url}/auth/verify-email?token={verification_token}"
|
||||
|
||||
# Template data
|
||||
template_data = {
|
||||
"username": username,
|
||||
"verification_link": verification_link,
|
||||
"base_url": base_url,
|
||||
}
|
||||
|
||||
# Send email
|
||||
await send_email(
|
||||
recipients=[email],
|
||||
subject="Verify Your Email - LeagueLedger",
|
||||
body="", # Empty as we're using a template
|
||||
template_name="email_verification.html",
|
||||
template_body=template_data,
|
||||
background_tasks=background_tasks,
|
||||
)
|
||||
email_to=None,
|
||||
username: str = None,
|
||||
verification_token: str = None,
|
||||
background_tasks: BackgroundTasks = None,
|
||||
email=None, # Added for backward compatibility
|
||||
):
|
||||
"""Send email verification link to newly registered users"""
|
||||
try:
|
||||
# Use email parameter if email_to is not provided
|
||||
recipient_email = email_to if email_to is not None else email
|
||||
|
||||
if not recipient_email:
|
||||
logger.error("No email address provided for verification email")
|
||||
return False
|
||||
|
||||
# Create verification URL with token
|
||||
verification_link = f"{APP_BASE_URL}/auth/verify-email?token={verification_token}"
|
||||
|
||||
# Get the email template
|
||||
template = env.get_template("email/email_verification.html")
|
||||
|
||||
# Render the HTML content with variables
|
||||
html_content = template.render(
|
||||
username=username,
|
||||
verification_link=verification_link
|
||||
)
|
||||
|
||||
# Send email
|
||||
subject = "Verify Your Email Address - LeagueLedger"
|
||||
await send_email(
|
||||
email_to=recipient_email,
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
logger.info(f"Verification email sent to {recipient_email}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send verification email: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
async def send_welcome_email(
|
||||
email: EmailStr,
|
||||
async def send_join_request_response(
|
||||
user_email: str,
|
||||
username: str,
|
||||
background_tasks: Optional[BackgroundTasks] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send welcome email to new users
|
||||
|
||||
Args:
|
||||
email: Recipient email address
|
||||
username: User's username
|
||||
background_tasks: Optional BackgroundTasks for sending in background
|
||||
"""
|
||||
# Get base URL from environment variables
|
||||
base_url = os.getenv("LEAGUELEDGER_BASE_URL", "http://localhost:8000")
|
||||
|
||||
# Template data
|
||||
template_data = {
|
||||
"username": username,
|
||||
"base_url": base_url,
|
||||
}
|
||||
|
||||
# Send email
|
||||
await send_email(
|
||||
recipients=[email],
|
||||
subject="Welcome to LeagueLedger!",
|
||||
body="", # Empty as we're using a template
|
||||
template_name="welcome.html",
|
||||
template_body=template_data,
|
||||
background_tasks=background_tasks,
|
||||
)
|
||||
team_name: str,
|
||||
is_approved: bool,
|
||||
background_tasks: BackgroundTasks
|
||||
):
|
||||
"""Send email notification about join request approval/denial"""
|
||||
try:
|
||||
# Get the email template
|
||||
template = env.get_template("email/join_request_response.html")
|
||||
|
||||
# Render the HTML content with variables
|
||||
html_content = template.render(
|
||||
username=username,
|
||||
team_name=team_name,
|
||||
is_approved=is_approved,
|
||||
base_url=APP_BASE_URL
|
||||
)
|
||||
|
||||
# Send email
|
||||
status = "Approved" if is_approved else "Denied"
|
||||
subject = f"Team Join Request {status} - {team_name}"
|
||||
await send_email(
|
||||
email_to=user_email,
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
background_tasks=background_tasks
|
||||
)
|
||||
logger.info(f"Join request response email sent to {user_email}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send join request response email: {str(e)}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user