add landing emails

This commit is contained in:
Даниил Ивлев 2026-01-15 20:41:24 +05:00
parent f4ee5f8069
commit 10b7108b40
8 changed files with 253 additions and 23 deletions

View File

@ -38,6 +38,18 @@ ELEVENLABS_API_KEY=your-elevenlabs-api-key-here
APP_ENV=development
DEBUG=true
# App Mode: 'landing' for landing page, 'app' for main application
APP_MODE=landing
# Email Configuration (for contact form)
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USE_TLS=true
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
DEFAULT_FROM_EMAIL=noreply@example.com
CONTACT_REQUEST_EMAIL=contact@example.com
DOMAIN=hr.aiquity.xyz
CLOUDMERSIVE_API_KEY=96e66e45-d236-4ab3-909d-f1a437d5940

View File

@ -1,5 +1,4 @@
# Caddyfile for HR AI Backend with automatic HTTPS
# Environment variable DOMAIN will be used, defaults to localhost
# Caddyfile for HR AI with automatic HTTPS
{$DOMAIN:localhost} {
# Backend API routes
@ -12,19 +11,22 @@
reverse_proxy backend:8000
}
# Frontend (SPA) - serve everything else
# OpenAPI docs
handle /docs {
reverse_proxy backend:8000
}
handle /redoc {
reverse_proxy backend:8000
}
handle /openapi.json {
reverse_proxy backend:8000
}
# Frontend - proxy everything else to Next.js
handle {
reverse_proxy frontend:3000
# SPA fallback - serve index.html for all non-API routes
@notapi {
not path /api/*
not path /health
file {
try_files {path} /index.html
}
}
rewrite @notapi /index.html
}
# Enable gzip compression
@ -32,15 +34,10 @@
# Security headers
header {
# HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# XSS Protection
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
X-XSS-Protection "1; mode=block"
# CORS for API (adjust origins as needed)
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Access-Control-Allow-Headers "Content-Type, Authorization"
@ -51,4 +48,4 @@
output file /var/log/caddy/access.log
format json
}
}
}

View File

@ -45,6 +45,18 @@ class Settings(BaseSettings):
app_env: str = "development"
debug: bool = True
# App Mode: 'landing' for landing page mode, 'app' for main application
app_mode: str = "landing"
# Email Configuration
email_host: str = "smtp.gmail.com"
email_port: int = 587
email_use_tls: bool = True
email_host_user: str = ""
email_host_password: str = ""
default_from_email: str = "noreply@example.com"
contact_request_email: str = "contact@example.com"
class Config:
env_file = ".env"
extra = "ignore"

View File

@ -0,0 +1,55 @@
from fastapi import Request
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from app.core.config import settings
class LandingModeMiddleware(BaseHTTPMiddleware):
"""
Middleware that restricts API access when app is in landing mode.
Only allows access to:
- /health
- /api/v1/contact
- /api/v1/config/mode
- / (root)
- /docs, /redoc, /openapi.json (API documentation)
"""
ALLOWED_PATHS = [
"/",
"/health",
"/docs",
"/redoc",
"/openapi.json",
]
ALLOWED_PREFIXES = [
"/api/v1/contact",
"/api/v1/config",
]
async def dispatch(self, request: Request, call_next):
# If not in landing mode, allow all requests
if settings.app_mode != "landing":
return await call_next(request)
path = request.url.path
# Check if path is in allowed list
if path in self.ALLOWED_PATHS:
return await call_next(request)
# Check if path starts with allowed prefix
for prefix in self.ALLOWED_PREFIXES:
if path.startswith(prefix):
return await call_next(request)
# Block all other API requests in landing mode
return JSONResponse(
status_code=403,
content={
"detail": "API недоступен в режиме лендинга. Свяжитесь с нами через форму обратной связи.",
"mode": "landing",
},
)

View File

@ -0,0 +1,44 @@
import logging
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr
from app.services.email_service import send_contact_request_email
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/contact", tags=["Contact"])
class ContactRequest(BaseModel):
name: str
email: EmailStr
phone: str | None = None
company: str | None = None
message: str | None = None
class ContactResponse(BaseModel):
detail: str
@router.post("/", response_model=ContactResponse, status_code=200)
async def create_contact_request(contact_data: ContactRequest):
"""
Handle contact request from landing page form.
Sends an email notification instead of saving to database.
"""
try:
await send_contact_request_email(
name=contact_data.name,
email=contact_data.email,
phone=contact_data.phone,
company=contact_data.company,
message=contact_data.message,
)
logger.info(f"Contact request email sent for {contact_data.email}")
return ContactResponse(detail="Contact request submitted successfully")
except Exception as e:
logger.error(f"Failed to send contact request email: {e}")
raise HTTPException(
status_code=500, detail="Failed to submit contact request. Please try again."
)

View File

@ -0,0 +1,86 @@
import asyncio
import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from app.core.config import settings
logger = logging.getLogger(__name__)
def _send_email_sync(
subject: str,
body: str,
to_email: str,
from_email: str | None = None,
) -> bool:
"""Synchronous email sending using smtplib."""
if not settings.email_host_user or not settings.email_host_password:
logger.warning("Email credentials not configured, skipping email send")
return False
from_email = from_email or settings.default_from_email
msg = MIMEMultipart()
msg["From"] = from_email
msg["To"] = to_email
msg["Subject"] = subject
msg.attach(MIMEText(body, "plain", "utf-8"))
try:
if settings.email_use_tls:
server = smtplib.SMTP(settings.email_host, settings.email_port)
server.starttls()
else:
server = smtplib.SMTP_SSL(settings.email_host, settings.email_port)
server.login(settings.email_host_user, settings.email_host_password)
server.send_message(msg)
server.quit()
return True
except Exception as e:
logger.error(f"Failed to send email: {e}")
raise
async def send_email(
subject: str,
body: str,
to_email: str,
from_email: str | None = None,
) -> bool:
"""Async wrapper for sending emails."""
return await asyncio.to_thread(
_send_email_sync,
subject,
body,
to_email,
from_email,
)
async def send_contact_request_email(
name: str,
email: str,
phone: str | None = None,
company: str | None = None,
message: str | None = None,
) -> bool:
"""Send contact request notification email."""
subject = f"New Contact Request from {name}"
body = f"""New contact request received:
Name: {name}
Email: {email}
Phone: {phone or 'Not specified'}
Company: {company or 'Not specified'}
Message:
{message or 'No additional message'}
"""
return await send_email(
subject=subject,
body=body,
to_email=settings.contact_request_email,
)

View File

@ -31,7 +31,7 @@ services:
# HR AI Backend
backend:
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-backend:latest
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-backend:latest
expose:
- "8000"
env_file:
@ -64,7 +64,7 @@ services:
# CELERY
celery:
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-backend:latest
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-backend:latest
env_file:
- .env
environment:
@ -105,7 +105,7 @@ services:
restart: unless-stopped
frontend:
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-frontend:latest
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-frontend:latest
platform: linux/amd64
expose:
- "3000"

26
main.py
View File

@ -5,10 +5,13 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.core.mode_middleware import LandingModeMiddleware
from app.core.session_middleware import SessionMiddleware
from app.routers import resume_router, vacancy_router
from app.routers.admin_router import router as admin_router
from app.routers.analysis_router import router as analysis_router
from app.routers.contact_router import router as contact_router
from app.routers.interview_reports_router import router as interview_report_router
from app.routers.interview_router import router as interview_router
from app.routers.session_router import router as session_router
@ -56,6 +59,9 @@ app.add_middleware(
# Добавляем middleware для управления сессиями (после CORS)
app.add_middleware(SessionMiddleware, cookie_name="session_id")
# Добавляем middleware для режима лендинга (блокирует API в режиме landing)
app.add_middleware(LandingModeMiddleware)
app.include_router(vacancy_router, prefix="/api/v1")
app.include_router(resume_router, prefix="/api/v1")
app.include_router(session_router, prefix="/api/v1")
@ -63,13 +69,31 @@ app.include_router(interview_router, prefix="/api/v1")
app.include_router(analysis_router, prefix="/api/v1")
app.include_router(admin_router, prefix="/api/v1")
app.include_router(interview_report_router, prefix="/api/v1")
app.include_router(contact_router, prefix="/api/v1")
@app.get("/")
async def root():
return {"message": "HR AI Backend API", "version": "1.0.0"}
return {
"message": "HR AI Backend API",
"version": "1.0.0",
"mode": settings.app_mode,
}
@app.get("/health")
async def health_check():
return {"status": "healthy"}
@app.get("/api/v1/config/mode")
async def get_app_mode():
"""
Get current application mode.
Returns 'landing' for landing page mode or 'app' for main application.
"""
return {
"mode": settings.app_mode,
"description": "landing" if settings.app_mode == "landing"
else "Main application mode",
}