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
|
APP_ENV=development
|
||||||
DEBUG=true
|
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
|
DOMAIN=hr.aiquity.xyz
|
||||||
|
|
||||||
CLOUDMERSIVE_API_KEY=96e66e45-d236-4ab3-909d-f1a437d5940
|
CLOUDMERSIVE_API_KEY=96e66e45-d236-4ab3-909d-f1a437d5940
|
||||||
|
|||||||
33
Caddyfile
33
Caddyfile
@ -1,5 +1,4 @@
|
|||||||
# Caddyfile for HR AI Backend with automatic HTTPS
|
# Caddyfile for HR AI with automatic HTTPS
|
||||||
# Environment variable DOMAIN will be used, defaults to localhost
|
|
||||||
|
|
||||||
{$DOMAIN:localhost} {
|
{$DOMAIN:localhost} {
|
||||||
# Backend API routes
|
# Backend API routes
|
||||||
@ -12,19 +11,22 @@
|
|||||||
reverse_proxy backend:8000
|
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 {
|
handle {
|
||||||
reverse_proxy frontend:3000
|
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
|
# Enable gzip compression
|
||||||
@ -32,15 +34,10 @@
|
|||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
header {
|
header {
|
||||||
# HSTS
|
|
||||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
|
||||||
# XSS Protection
|
|
||||||
X-Content-Type-Options "nosniff"
|
X-Content-Type-Options "nosniff"
|
||||||
X-Frame-Options "DENY"
|
X-Frame-Options "DENY"
|
||||||
X-XSS-Protection "1; mode=block"
|
X-XSS-Protection "1; mode=block"
|
||||||
|
|
||||||
# CORS for API (adjust origins as needed)
|
|
||||||
Access-Control-Allow-Origin "*"
|
Access-Control-Allow-Origin "*"
|
||||||
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||||
Access-Control-Allow-Headers "Content-Type, Authorization"
|
Access-Control-Allow-Headers "Content-Type, Authorization"
|
||||||
|
|||||||
@ -45,6 +45,18 @@ class Settings(BaseSettings):
|
|||||||
app_env: str = "development"
|
app_env: str = "development"
|
||||||
debug: bool = True
|
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:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
extra = "ignore"
|
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
|
# HR AI Backend
|
||||||
backend:
|
backend:
|
||||||
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-backend:latest
|
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-backend:latest
|
||||||
expose:
|
expose:
|
||||||
- "8000"
|
- "8000"
|
||||||
env_file:
|
env_file:
|
||||||
@ -64,7 +64,7 @@ services:
|
|||||||
|
|
||||||
# CELERY
|
# CELERY
|
||||||
celery:
|
celery:
|
||||||
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-backend:latest
|
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-backend:latest
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@ -105,7 +105,7 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: cr.yandex/crp9p5rtbnbop36duusi/hr-ai-frontend:latest
|
image: cr.yandex/crpnbn7b142t058unrn0/hr-ai-frontend:latest
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
|
|||||||
26
main.py
26
main.py
@ -5,10 +5,13 @@ from contextlib import asynccontextmanager
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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.core.session_middleware import SessionMiddleware
|
||||||
from app.routers import resume_router, vacancy_router
|
from app.routers import resume_router, vacancy_router
|
||||||
from app.routers.admin_router import router as admin_router
|
from app.routers.admin_router import router as admin_router
|
||||||
from app.routers.analysis_router import router as analysis_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_reports_router import router as interview_report_router
|
||||||
from app.routers.interview_router import router as interview_router
|
from app.routers.interview_router import router as interview_router
|
||||||
from app.routers.session_router import router as session_router
|
from app.routers.session_router import router as session_router
|
||||||
@ -56,6 +59,9 @@ app.add_middleware(
|
|||||||
# Добавляем middleware для управления сессиями (после CORS)
|
# Добавляем middleware для управления сессиями (после CORS)
|
||||||
app.add_middleware(SessionMiddleware, cookie_name="session_id")
|
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(vacancy_router, prefix="/api/v1")
|
||||||
app.include_router(resume_router, prefix="/api/v1")
|
app.include_router(resume_router, prefix="/api/v1")
|
||||||
app.include_router(session_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(analysis_router, prefix="/api/v1")
|
||||||
app.include_router(admin_router, prefix="/api/v1")
|
app.include_router(admin_router, prefix="/api/v1")
|
||||||
app.include_router(interview_report_router, prefix="/api/v1")
|
app.include_router(interview_report_router, prefix="/api/v1")
|
||||||
|
app.include_router(contact_router, prefix="/api/v1")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
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")
|
@app.get("/health")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
return {"status": "healthy"}
|
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