add landing emails
This commit is contained in:
parent
f4ee5f8069
commit
10b7108b40
12
.env.example
12
.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
|
||||
|
||||
35
Caddyfile
35
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
55
app/core/mode_middleware.py
Normal file
55
app/core/mode_middleware.py
Normal 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",
|
||||
},
|
||||
)
|
||||
44
app/routers/contact_router.py
Normal file
44
app/routers/contact_router.py
Normal 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."
|
||||
)
|
||||
86
app/services/email_service.py
Normal file
86
app/services/email_service.py
Normal 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,
|
||||
)
|
||||
@ -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
26
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",
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user