From 10b7108b406be18744664ba422278d138aa08ccf Mon Sep 17 00:00:00 2001 From: tdjx Date: Thu, 15 Jan 2026 20:41:24 +0500 Subject: [PATCH] add landing emails --- .env.example | 12 +++++ Caddyfile | 35 +++++++------- app/core/config.py | 12 +++++ app/core/mode_middleware.py | 55 ++++++++++++++++++++++ app/routers/contact_router.py | 44 ++++++++++++++++++ app/services/email_service.py | 86 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 6 +-- main.py | 26 ++++++++++- 8 files changed, 253 insertions(+), 23 deletions(-) create mode 100644 app/core/mode_middleware.py create mode 100644 app/routers/contact_router.py create mode 100644 app/services/email_service.py diff --git a/.env.example b/.env.example index 979c3c0..deb0a88 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/Caddyfile b/Caddyfile index 6c10164..cebc005 100644 --- a/Caddyfile +++ b/Caddyfile @@ -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 } -} \ No newline at end of file +} diff --git a/app/core/config.py b/app/core/config.py index e5fd88e..8e61c1e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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" diff --git a/app/core/mode_middleware.py b/app/core/mode_middleware.py new file mode 100644 index 0000000..7ee01ec --- /dev/null +++ b/app/core/mode_middleware.py @@ -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", + }, + ) diff --git a/app/routers/contact_router.py b/app/routers/contact_router.py new file mode 100644 index 0000000..ef04247 --- /dev/null +++ b/app/routers/contact_router.py @@ -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." + ) diff --git a/app/services/email_service.py b/app/services/email_service.py new file mode 100644 index 0000000..32f57cb --- /dev/null +++ b/app/services/email_service.py @@ -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, + ) diff --git a/docker-compose.yml b/docker-compose.yml index ba98b69..1d92442 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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" diff --git a/main.py b/main.py index 8533276..ed17b8d 100644 --- a/main.py +++ b/main.py @@ -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", + }