Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions backend/alembic/versions/008_add_meeting_participants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""add meeting participants

Revision ID: 008_add_meeting_participants
Revises: 007_add_assignees
Create Date: 2026-02-10 17:30:00.000000

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "008_add_meeting_participants"
down_revision = "007_add_assignees"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"meeting_participants",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("meeting_id", sa.Integer(), nullable=False),
sa.Column("name", sa.String(length=200), nullable=False),
sa.Column("email", sa.String(length=200), nullable=False),
sa.Column("source", sa.String(length=50), nullable=True, server_default="manual"),
sa.Column("created_at", sa.DateTime(), nullable=True, server_default=sa.func.now()),
sa.ForeignKeyConstraint(["meeting_id"], ["meetings.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("meeting_id", "email", name="uq_meeting_participant_email"),
)
op.create_index("ix_meeting_participants_meeting_id", "meeting_participants", ["meeting_id"])
op.create_index("ix_meeting_participants_email", "meeting_participants", ["email"])


def downgrade():
op.drop_index("ix_meeting_participants_email", table_name="meeting_participants")
op.drop_index("ix_meeting_participants_meeting_id", table_name="meeting_participants")
op.drop_table("meeting_participants")
16 changes: 16 additions & 0 deletions backend/alembic/versions/integrated_all_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,21 @@ def upgrade():
)
op.create_index('idx_reminder_scheduled_status', 'meeting_reminders', ['scheduled_at', 'status'])

if not _table_exists(conn, 'meeting_participants'):
op.create_table(
'meeting_participants',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('meeting_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(200), nullable=False),
sa.Column('email', sa.String(200), nullable=False),
sa.Column('source', sa.String(50), nullable=True, server_default='manual'),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['meeting_id'], ['meetings.id'], ondelete='CASCADE'),
sa.UniqueConstraint('meeting_id', 'email', name='uq_meeting_participant_email'),
)
op.create_index('ix_meeting_participants_meeting_id', 'meeting_participants', ['meeting_id'])
op.create_index('ix_meeting_participants_email', 'meeting_participants', ['email'])

# seed default community and admin
# insert only if not exists
users = conn.execute(sa.text("SELECT id FROM users WHERE username = :u OR email = :e"), {"u": "admin", "e": "admin@example.com"}).fetchone()
Expand Down Expand Up @@ -270,6 +285,7 @@ def downgrade():

# drop tables in reverse order if exist
for t in [
'meeting_participants',
'meeting_reminders',
'meetings',
'committee_members',
Expand Down
182 changes: 181 additions & 1 deletion backend/app/api/communities.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from pydantic import BaseModel
from sqlalchemy.orm import Session, attributes
from sqlalchemy import update, select, insert

from app.core.dependencies import get_current_user, get_current_active_superuser
Expand All @@ -17,10 +18,16 @@
CommunityMemberAdd,
UserBrief,
)
from app.schemas.email import EmailSettings, EmailSettingsOut


router = APIRouter()


class TestEmailRequest(BaseModel):
to_email: str


@router.get("", response_model=List[CommunityBrief])
def list_communities(
current_user: User = Depends(get_current_user),
Expand Down Expand Up @@ -437,3 +444,176 @@ def update_user_role(
db.commit()

return {"message": f"User role updated to {role} successfully"}


@router.get("/{community_id}/email-settings", response_model=EmailSettingsOut)
def get_email_settings(
community_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Get email settings for a community."""
community = db.query(Community).filter(Community.id == community_id).first()
if not community:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Community not found",
)

# Check if user has access to this community
if not current_user.is_superuser and community not in current_user.communities:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied",
)

settings_data = community.settings or {}
email_config = settings_data.get("email") if isinstance(settings_data, dict) else None

if not email_config:
# Return default empty settings
return EmailSettingsOut(
enabled=False,
provider="smtp",
from_email="",
smtp={}
)

# Return all SMTP config including password (admin has permission to view)
smtp_config = email_config.get("smtp", {})
if not isinstance(smtp_config, dict):
smtp_config = {}

return EmailSettingsOut(
enabled=email_config.get("enabled", False),
provider=email_config.get("provider", "smtp"),
from_email=email_config.get("from_email", ""),
from_name=email_config.get("from_name"),
reply_to=email_config.get("reply_to"),
smtp=smtp_config
)


@router.put("/{community_id}/email-settings")
def update_email_settings(
community_id: int,
settings: EmailSettings,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Update email settings for a community. Admin only."""
community = db.query(Community).filter(Community.id == community_id).first()
if not community:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Community not found",
)

# Check if user is admin of this community or superuser
is_admin = False
if current_user.is_superuser:
is_admin = True
else:
# Check community_users role
result = db.execute(
select(community_users.c.role)
.where(community_users.c.user_id == current_user.id)
.where(community_users.c.community_id == community_id)
).first()
if result and result[0] == 'admin':
is_admin = True

if not is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required",
)

# Update settings
current_settings = community.settings or {}
if not isinstance(current_settings, dict):
current_settings = {}

current_settings["email"] = settings.model_dump()
community.settings = current_settings
# Mark the JSON field as modified so SQLAlchemy detects the change
attributes.flag_modified(community, "settings")

db.commit()
db.refresh(community)

return {"message": "Email settings updated successfully"}


@router.post("/{community_id}/email-settings/test")
def test_email_settings(
community_id: int,
request_data: TestEmailRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""Test email settings by sending a test email. Admin only."""
to_email = request_data.to_email
if not to_email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="to_email is required",
)

community = db.query(Community).filter(Community.id == community_id).first()
if not community:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Community not found",
)

# Check if user is admin of this community or superuser
is_admin = False
if current_user.is_superuser:
is_admin = True
else:
# Check community_users role
result = db.execute(
select(community_users.c.role)
.where(community_users.c.user_id == current_user.id)
.where(community_users.c.community_id == community_id)
).first()
if result and result[0] == 'admin':
is_admin = True

if not is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required",
)

# Send test email
from app.services.email import send_email, EmailMessage, get_sender_info, get_smtp_config

smtp_config, email_cfg = get_smtp_config(community)
if not smtp_config:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="SMTP not configured",
)

from_email, from_name, reply_to = get_sender_info(community, email_cfg)

message = EmailMessage(
subject=f"[{community.name}] Email Configuration Test",
to_emails=[to_email],
html_body="<html><body><h2>Email Test Successful!</h2><p>Your SMTP configuration is working correctly.</p></body></html>",
text_body="Email Test Successful!\n\nYour SMTP configuration is working correctly.",
from_email=from_email,
from_name=from_name,
reply_to=reply_to,
)

try:
send_email(community, message)
return {"message": "Test email sent successfully"}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to send email: {str(e)}",
)
Loading
Loading