Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ DATABASE_URL=postgresql+asyncpg://trusthire:trusthire@localhost:5432/trusthire
REDIS_URL=redis://localhost:6379

# Stripe — https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY=sk_test_YOUR_STRIPE_SECRET_KEY
# If you accidentally exposed live keys, rotate them immediately in Stripe Dashboard.
STRIPE_SECRET_KEY=sk_live_OR_sk_test_YOUR_STRIPE_SECRET_KEY
STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_STRIPE_PUBLISHABLE_KEY
STRIPE_WEBHOOK_SECRET=whsec_YOUR_WEBHOOK_SECRET

Expand All @@ -36,3 +37,10 @@ CORS_ORIGINS=http://localhost:3000,http://localhost:8000,https://felipeofdev-ai.
SENTRY_DSN=
LOG_LEVEL=INFO
LOG_FORMAT=json

# OpenAI (GPT/Codex Router)
OPENAI_API_KEY=

STRIPE_SUCCESS_URL=https://trusthire.ai/success
STRIPE_CANCEL_URL=https://trusthire.ai/cancel
STRIPE_TRIAL_DAYS=7
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: CI

on:
push:
branches: ["main", "work"]
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest
- name: Run tests
run: pytest -q
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Secrets and infra state
.env
*.pem
secrets/
weights/
terraform.tfstate
terraform.tfstate.*

# Python
__pycache__/
*.pyc
.venv/
venv/

# Node
node_modules/
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,107 @@ Contributions welcome! Please:
---

**Built with ❤️ to protect job seekers from scams**


## ✅ Production Readiness Checklist

- Deploy público online: Railway (`render.yaml`/`railway.toml`) e health endpoint `/health`.
- Login real: JWT com refresh token e API keys.
- Banco real: suporte a PostgreSQL via `DATABASE_URL`.
- API documentada: Swagger em `/api/v1/docs`.
- Testes unitários: suíte `tests/` + CI em `.github/workflows/ci.yml`.
- Monitoramento e métricas: endpoint `/metrics` com uptime, latência média/p95, RPM e taxa de erro.
- Segurança básica: rate limiting, headers CSP/XSS, sanitização server-side e hash Argon2.

## Stripe (modo real)
1. No Dashboard Stripe, trocar para **Live mode**.
2. Configurar variáveis no Railway:
- `STRIPE_SECRET_KEY`
- `STRIPE_PUBLISHABLE_KEY`
- `STRIPE_WEBHOOK_SECRET`
3. Criar produtos/preços live e preencher os `STRIPE_PRICE_*`.
4. Validar checkout no endpoint `/api/v1/billing`.

## Railway (produção)
1. Definir variáveis de ambiente de produção (`ENV=prod`, `SECRET_KEY`, `DATABASE_URL`, `REDIS_URL`).
2. Garantir `CORS_ORIGINS_STR` com domínio público do frontend.
3. Monitorar `/health` e `/metrics` no painel de observabilidade.


## 🚀 Deploy de Produção (script completo)

```bash
bash scripts/deploy_production.sh
```

Variáveis opcionais:
- `REGISTRY_URI` (ex: ECR/GHCR)
- `IMAGE_TAG` (default: `latest`)

## 📈 Monitoramento pronto + Dashboard pronto

```bash
docker compose -f docker-compose.monitoring.yml up -d
```

Acessos:
- Prometheus: `http://localhost:9090`
- Grafana: `http://localhost:3001` (`admin/admin`)
- Loki: `http://localhost:3100`

Métricas Prometheus da API:
- `GET /metrics/prometheus`

## ⚡ Load test pronto

```bash
bash scripts/run_load_test.sh http://localhost:8000
```

Saída de benchmark:
- `loadtest/results/benchmark.md`
- CSVs do Locust em `loadtest/results/`


## 💳 Stripe Live (produção)

Guia operacional completo em `docs/STRIPE_LIVE_SETUP.md`.

Script para criação de produtos/preços live:
```bash
export STRIPE_SECRET_KEY=sk_live_...
bash scripts/stripe_setup_live.sh
```


Teste local do webhook com Stripe CLI:
```bash
bash scripts/stripe_webhook_local_test.sh http://localhost:8000
```


## 🌐 Provas Públicas de Senioridade

### Demo pública
- Live Demo: `https://app.trusthire.ai` (configure apontamento para seu deploy em produção)

### Screenshot do dashboard
- Suba a stack de monitoramento:
```bash
docker compose -f docker-compose.monitoring.yml up -d
```
- Acesse Grafana em `http://localhost:3001` e exporte screenshot do dashboard "TrustHire Production Overview".

### Benchmark publicado
```bash
bash scripts/run_load_test.sh http://localhost:8000
python scripts/generate_benchmark_summary.py
```
- Resultado público em: `docs/BENCHMARK_PUBLIC.md`

### Architecture diagram
- Arquitetura: `docs/ARCHITECTURE_DIAGRAM.md`
- Diagrama SVG: `docs/architecture.svg`

### Case study técnico
- Documento: `docs/CASE_STUDY_TECHNICAL.md`
18 changes: 18 additions & 0 deletions README_INTEGRACAO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# 🚀 TrustHire - Guia Rápido de Integração


## ✅ Integração automática (recomendado)

No repositório `trusthire`, execute:

```bash
bash scripts/link_trusthire_ecossistema.sh
```

Esse script:
- clona/atualiza `trusthire-backend` e `trusthire-frontend` como diretórios irmãos;
- ajusta `ALLOWED_ORIGINS` no backend para aceitar frontend React e `index.html`;
- cria `trusthire-frontend/.env.local` apontando para `http://localhost:8000/api/v1`.

Depois, suba os 3 serviços conforme instruções exibidas pelo script.

---

## ⚡ Setup em 5 Minutos

### 1️⃣ Clone os 3 Repositórios
Expand Down
31 changes: 31 additions & 0 deletions ai/router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Multi-provider AI router (Claude + GPT + Codex) with safe fallback."""

from __future__ import annotations

from typing import Literal

from config import settings

TaskType = Literal["resume", "scam", "code"]


def ai_router(task: TaskType, prompt: str) -> dict:
"""
Route task to preferred provider.
This keeps production routing explicit while allowing env-based gradual rollout.
"""
provider = "fallback"

if task == "scam" and settings.ANTHROPIC_API_KEY:
provider = "claude"
elif task == "resume" and settings.OPENAI_API_KEY:
provider = "gpt"
elif task == "code" and settings.OPENAI_API_KEY:
provider = "codex"

# Controlled fallback (no external call in offline/dev env)
return {
"provider": provider,
"task": task,
"response": f"[{provider}] {prompt[:400]}",
}
7 changes: 6 additions & 1 deletion api/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
Core analysis endpoints with per-tier rate limiting
"""

from fastapi import APIRouter, HTTPException, Depends
from fastapi import APIRouter, HTTPException, Depends, Request

from models.schemas import AnalysisRequest, AnalysisResult
from models.user_models import TokenData, UserTier
from core.analyzer import get_analyzer
from auth.auth_service import get_current_user
from database.user_repository import UserRepository
from utils.logger import get_logger
from utils.security import sanitize_user_text
from utils.audit import log_audit_event

logger = get_logger("api.analysis")
router = APIRouter()
Expand Down Expand Up @@ -40,6 +42,7 @@ async def enforce_rate_limit(user: TokenData) -> None:
async def analyze_message(
request: AnalysisRequest,
user: TokenData = Depends(get_current_user),
http_request: Request = None,
):
"""
Analyze a recruitment message for scam indicators.
Expand All @@ -58,6 +61,7 @@ async def analyze_message(

try:
analyzer = get_analyzer()
request.text = sanitize_user_text(request.text)
result = await analyzer.analyze(
text=request.text,
include_ai=request.include_ai_analysis and user.tier != UserTier.FREE,
Expand All @@ -67,6 +71,7 @@ async def analyze_message(
if user.user_id != "anonymous":
db = UserRepository()
await db.increment_analysis_count(user.user_id)
log_audit_event("analysis.run", user.user_id, request=http_request)
return result
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
Expand Down
19 changes: 15 additions & 4 deletions api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from datetime import timedelta
from typing import Optional

from fastapi import APIRouter, HTTPException, Depends, status, BackgroundTasks
from fastapi import APIRouter, HTTPException, Depends, status, BackgroundTasks, Request
from fastapi.security import HTTPAuthorizationCredentials

from models.user_models import (
Expand All @@ -22,6 +22,8 @@
)
from database.user_repository import UserRepository
from utils.logger import get_logger
from utils.audit import log_audit_event
from utils.security import sanitize_user_text

logger = get_logger("api.auth")
router = APIRouter(prefix="/auth", tags=["Authentication"])
Expand All @@ -32,6 +34,7 @@
async def register(
request: RegisterRequest,
background_tasks: BackgroundTasks,
http_request: Request,
):
"""
Register a new user account.
Expand All @@ -49,10 +52,11 @@ async def register(
)

# Create user
safe_name = sanitize_user_text(request.name)
hashed = hash_password(request.password)
user = await db.create_user(
email=request.email,
name=request.name,
name=safe_name,
hashed_password=hashed,
)

Expand All @@ -66,6 +70,7 @@ async def register(
refresh_token = create_refresh_token(user.id)

logger.info("user_registered", extra={"user_id": user.id})
log_audit_event("auth.register", user.id, request=http_request)

return Token(
access_token=access_token,
Expand All @@ -77,7 +82,7 @@ async def register(
# ==================== LOGIN ====================

@router.post("/login", response_model=Token)
async def login(request: LoginRequest):
async def login(request: LoginRequest, http_request: Request):
"""
Login with email and password.

Expand Down Expand Up @@ -105,6 +110,7 @@ async def login(request: LoginRequest):
refresh_token = create_refresh_token(user.id)

logger.info("user_login", extra={"user_id": user.id, "tier": user.tier.value})
log_audit_event("auth.login", user.id, request=http_request)

return Token(
access_token=access_token,
Expand Down Expand Up @@ -166,6 +172,7 @@ async def get_me(user=Depends(require_auth)):
async def change_password(
request: ChangePasswordRequest,
user=Depends(require_auth),
http_request: Request = None,
):
"""Change account password"""
db = UserRepository()
Expand All @@ -182,6 +189,7 @@ async def change_password(
new_hash = hash_password(request.new_password)
await db.update_password(user.user_id, new_hash)
logger.info("password_changed", extra={"user_id": user.user_id})
log_audit_event("auth.change_password", user.user_id, request=http_request)


# ==================== API KEYS ====================
Expand All @@ -190,6 +198,7 @@ async def change_password(
async def create_api_key(
request: APIKeyCreate,
user=Depends(require_auth),
http_request: Request = None,
):
"""
Generate a new API key for programmatic access.
Expand All @@ -205,6 +214,7 @@ async def create_api_key(

from datetime import datetime
logger.info("api_key_generated", extra={"user_id": user.user_id})
log_audit_event("auth.api_key_generated", user.user_id, request=http_request)

return APIKeyResponse(
key=raw_key,
Expand All @@ -215,8 +225,9 @@ async def create_api_key(


@router.delete("/api-keys", status_code=status.HTTP_204_NO_CONTENT)
async def revoke_api_key(user=Depends(require_auth)):
async def revoke_api_key(user=Depends(require_auth), http_request: Request = None):
"""Revoke current API key"""
db = UserRepository()
await db.update_api_key(user.user_id, None)
logger.info("api_key_revoked", extra={"user_id": user.user_id})
log_audit_event("auth.api_key_revoked", user.user_id, request=http_request)
Loading