Система электронной почты

Обзор

Проект включает в себя систему верификации электронной почты, которая обеспечивает безопасность и подтверждение подлинности зарегистрированных пользователей. Система использует Google SMTP для отправки электронных писем и построена на следующих принципах:

  • Верификация по токену – уникальный токен генерируется при регистрации и отправляется на указанный email
  • Возможность повторной отправки письма верификации
  • Безопасное хранение данных пользователей
  • Ограничение функциональности для неподтвержденных аккаунтов

Настройка параметров SMTP

По умолчанию проект настроен для работы с Gmail SMTP. Для настройки отправки электронной почты необходимо изменить следующие переменные окружения в файле .env:

# SMTP настройки
SMTP_HOST=connect.smtp.bz
SMTP_PORT=587
SMTP_USER=support@devship.ru
SMTP_PASSWORD="your-password"
EMAILS_FROM_EMAIL=support@devship.ru
EMAILS_FROM_NAME="Название вашего проекта"

# Для SMTP сервиса необходимо использовать правильные учетные данные
# TLS порт: 587, SSL порт: 465

Важное примечание о Google SMTP

Для использования Gmail в качестве SMTP-сервера необходимо:

  1. Включить двухфакторную аутентификацию в настройках аккаунта Google
  2. Сгенерировать пароль приложения в настройках безопасности
  3. Использовать этот пароль приложения вместо обычного пароля в настройках SMTP

Основные параметры SMTP и отправки электронной почты настраиваются в файле backend/app/core/config.py:

# Email
SMTP_TLS: bool = True
SMTP_PORT: int = 587
SMTP_HOST: str = "connect.smtp.bz"
SMTP_USER: str = os.getenv("SMTP_USER", "")
SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "")
EMAILS_FROM_EMAIL: Optional[EmailStr] = os.getenv("EMAILS_FROM_EMAIL")
EMAILS_FROM_NAME: Optional[str] = os.getenv("EMAILS_FROM_NAME")

EMAIL_TEMPLATES_DIR: str = "app/email-templates"
EMAILS_ENABLED: bool = False  # Автоматически становится True при наличии всех необходимых настроек

Если вы хотите использовать другой SMTP-сервер, измените параметры SMTP_HOST, SMTP_PORTи SMTP_TLS в соответствии с инструкциями вашего провайдера электронной почты.

Процесс верификации email

1. Регистрация пользователя

При регистрации нового пользователя происходит следующее:

  1. Проверка, не занят ли указанный email другим пользователем
  2. Создание записи пользователя в базе данных
  3. Генерация уникального токена верификации
  4. Отправка письма с ссылкой для подтверждения email

Соответствующий код находится в файле backend/app/api/routes/auth.py в функции register:

@router.post("/register", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
async def register(
    db: AsyncDB,
    user_in: UserCreate,
) -> Any:
    """
    Register a new user
    """
    # Check if user already exists
    user = await get_user_by_email(db, email=user_in.email)
    if user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Email already registered",
        )

    # Create new user
    user = await create_user(db, obj_in=user_in)

    # Generate verification token
    token = generate_verification_token()
    await create_verification_token_for_user(db, user_id=user.id, token=token)

    # Send verification email
    send_verification_email(
        email_to=user.email,
        token=token,
        username=user.full_name or user.email,
    )

    return user

Примечание

В текущей версии кода верификация email временно отключена (закомментирована). Для включения верификации нужно раскомментировать соответствующие строки кода в функции register.

2. Подтверждение email

Когда пользователь переходит по ссылке из письма, происходит:

  1. Проверка токена на валидность и срок действия
  2. Маркировка токена как использованного
  3. Подтверждение email пользователя

Обработка подтверждения происходит в эндпоинте /api/v1/auth/verify-email:

@router.get("/verify-email", response_model=UserSchema)
async def verify_email(
    db: AsyncDB,
    token: str = Query(..., description="Email verification token"),
) -> Any:
    """
    Verify email address
    """
    # Get verification token
    verification = await get_valid_email_verification(db, token=token)
    if not verification:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Invalid or expired verification token",
        )

    # Mark token as used
    await mark_email_verification_as_used(db, db_obj=verification)

    # Mark user as verified
    user = await mark_user_verified(db, user_id=verification.user_id)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="User not found",
        )

    return user

3. Повторная отправка письма верификации

Если пользователь не получил или потерял письмо с подтверждением, он может запросить повторную отправку:

@router.post("/resend-verification", response_model=UserSchema)
async def resend_verification(
    db: AsyncDB,
    current_user: Annotated[User, Depends(get_current_active_user)],
) -> Any:
    """
    Resend verification email
    """
    if current_user.is_verified:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Email already verified",
        )

    # Generate verification token
    token = generate_verification_token()
    await create_verification_token_for_user(db, user_id=current_user.id, token=token)

    # Send verification email
    send_verification_email(
        email_to=current_user.email,
        token=token,
        username=current_user.full_name or current_user.email,
    )

    return current_user

Шаблоны электронных писем

Шаблоны электронных писем находятся в директории backend/app/email-templates/. В проекте используется система шаблонов Jinja2, что позволяет создавать динамические и хорошо оформленные письма.

Основной шаблон для верификации email: email_verification.html

Для настройки внешнего вида писем вы можете отредактировать этот шаблон или создать новые в той же директории. При модификации шаблонов доступны следующие переменные:

  • verification_url - URL для верификации email
  • username - имя пользователя или его email
  • project_name - название проекта

Использование этих переменных в шаблоне:

<h1>Здравствуйте, {{ username }}!</h1>
<p>Для подтверждения вашего email адреса в {{ project_name }}, пожалуйста, перейдите по ссылке:</p>
<a href="{{ verification_url }}">Подтвердить email</a>

Модель данных

Для хранения информации о верификации email используется таблица email_verifications, определенная моделью EmailVerification в файле backend/app/models/verification.py:

class EmailVerification(Base):
    id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
    token: Mapped[str] = mapped_column(String, unique=True, index=True)
    user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"))
    is_used: Mapped[bool] = mapped_column(Boolean, default=False)
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), default=func.now()
    )
    expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
    
    user: Mapped["User"] = relationship("User", back_populates="email_verifications")

Связь с пользователем определена в модели User в файле backend/app/models/user.py:

email_verifications: Mapped[List["EmailVerification"]] = relationship(
    "EmailVerification", back_populates="user", cascade="all, delete-orphan"
)

Работа с электронной почтой в коде

Основная логика работы с электронной почтой определена в файле backend/app/utils/email.py:

1. Генерация токена

def generate_verification_token() -> str:
    """
    Generate a random token for email verification
    """
    return secrets.token_urlsafe(32)

2. Отправка письма

def send_email(
    email_to: str,
    subject_template: str = "",
    html_template: str = "",
    environment: Dict[str, Any] = {},
) -> None:
    """
    Send an email using the configured SMTP server
    """
    assert settings.EMAILS_ENABLED, "No email settings configured"
    
    message = emails.Message(
        subject=JinjaTemplate(subject_template),
        html=JinjaTemplate(html_template),
        mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
    )
    
    smtp_options = {
        "host": settings.SMTP_HOST,
        "port": settings.SMTP_PORT,
    }
    
    if settings.SMTP_TLS:
        smtp_options["tls"] = True
    
    if settings.SMTP_USER:
        smtp_options["user"] = settings.SMTP_USER
    
    if settings.SMTP_PASSWORD:
        smtp_options["password"] = settings.SMTP_PASSWORD
    
    response = message.send(to=email_to, render=environment, smtp=smtp_options)
    logging.info(f"Email sent to {email_to}, status: {response.status_code}")

3. Отправка письма верификации

def send_verification_email(email_to: str, token: str, username: str) -> None:
    """
    Send an email verification email
    """
    if not settings.EMAILS_ENABLED:
        logging.warning("Emails not enabled, skipping verification email")
        return
    
    verification_url = f"{settings.SERVER_HOST}{settings.API_V1_STR}/auth/verify-email?token={token}"
    
    # Load template from file
    template_dir = Path(settings.EMAIL_TEMPLATES_DIR)
    env = Environment(loader=FileSystemLoader(template_dir))
    template = env.get_template("email_verification.html")
    
    subject = f"{settings.PROJECT_NAME} - Email Verification"
    html_content = template.render(
        verification_url=verification_url,
        username=username,
        project_name=settings.PROJECT_NAME,
    )
    
    send_email(
        email_to=email_to,
        subject_template=subject,
        html_template=html_content,
    )

Настройка для собственного проекта

Для настройки системы электронной почты под ваш проект, выполните следующие шаги:

  1. Создайте или модифицируйте файл .env в корне проекта и добавьте переменные SMTP
  2. При необходимости измените параметры SMTP-сервера в backend/app/core/config.py
  3. Отредактируйте шаблоны писем в backend/app/email-templates/ под свой дизайн
  4. Убедитесь, что функция верификации email включена в коде регистрации (раскомментируйте соответствующие строки)

Советы по безопасности

  • Никогда не храните пароли SMTP в исходном коде
  • Используйте переменные окружения или файл .env для хранения чувствительной информации
  • Всегда используйте TLS для защиты сообщений электронной почты
  • Регулярно меняйте пароли приложений для доступа к SMTP