feat: fix registration 500, add forgot-password flow, frontend validation
Some checks failed
Some checks failed
- Fix MissingGreenlet: sync_engine now uses psycopg2 instead of asyncpg - Fix bcrypt/passlib compat: pin bcrypt<4.1 in requirements - Fix legacy password_hash NOT NULL: alter column to nullable in migration - Add frontend password validation (uppercase + lowercase + digit) - Add forgot-password and reset-password backend endpoints - Add forgot-password and reset-password frontend pages - Add email_service.py (SMTP via admin settings) - Add reset_token/reset_token_expires columns to User model - Migrate legacy JSON-only users to DB on password reset request - Mount data/ volume in docker-compose.local.yml for persistence - Add production deployment config (Dockerfile, nginx, deploy.sh) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
466
deploy.sh
Executable file
466
deploy.sh
Executable file
@@ -0,0 +1,466 @@
|
||||
#!/bin/bash
|
||||
# ============================================================
|
||||
# Office Translator - Deployment Script
|
||||
# ============================================================
|
||||
# Usage: ./deploy.sh <command> [options]
|
||||
#
|
||||
# Commands:
|
||||
# start Build and start all services
|
||||
# stop Stop all services
|
||||
# restart Restart all services
|
||||
# status Show services status
|
||||
# logs Show logs (follow mode)
|
||||
# build Build/rebuild images
|
||||
# health Run health checks
|
||||
# backup Backup PostgreSQL database
|
||||
# clean Remove all containers, volumes, and images
|
||||
# shell Open a shell in the backend container
|
||||
# migrate Run database migrations
|
||||
# help Show this help message
|
||||
#
|
||||
# Options:
|
||||
# --env <file> Use a specific env file (default: .env.docker)
|
||||
# --prod Use production compose file (docker-compose.yml)
|
||||
# --no-build Skip build step on start
|
||||
# --rebuild Force rebuild without cache
|
||||
#
|
||||
# Examples:
|
||||
# ./deploy.sh start # Start with defaults (local)
|
||||
# ./deploy.sh start --rebuild # Force rebuild
|
||||
# ./deploy.sh start --prod # Start production stack
|
||||
# ./deploy.sh logs backend # Show backend logs
|
||||
# ./deploy.sh logs # Show all logs
|
||||
# ./deploy.sh backup # Backup database
|
||||
# ./deploy.sh clean # Full cleanup
|
||||
# ============================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---- Colors ----
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ---- Configuration ----
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
COMPOSE_LOCAL="docker-compose.local.yml"
|
||||
COMPOSE_PROD="docker-compose.yml"
|
||||
ENV_FILE=".env.docker"
|
||||
COMPOSE_FILE="$COMPOSE_LOCAL"
|
||||
COMMAND=""
|
||||
SERVICE=""
|
||||
NO_BUILD=false
|
||||
REBUILD=false
|
||||
|
||||
# ---- Helper Functions ----
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
header() { echo ""; echo -e "${BOLD}${CYAN}========================================${NC}"; echo -e "${BOLD}${CYAN} $*${NC}"; echo -e "${BOLD}${CYAN}========================================${NC}"; }
|
||||
|
||||
# Run docker compose with the right file and env
|
||||
dc() {
|
||||
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" "$@"
|
||||
}
|
||||
|
||||
# ---- Parse Arguments ----
|
||||
|
||||
COMMAND="${1:-help}"
|
||||
shift 2>/dev/null || true
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--env)
|
||||
ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--prod)
|
||||
COMPOSE_FILE="$COMPOSE_PROD"
|
||||
shift
|
||||
;;
|
||||
--no-build)
|
||||
NO_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--rebuild)
|
||||
REBUILD=true
|
||||
shift
|
||||
;;
|
||||
backend|frontend|postgres|redis|nginx|ollama)
|
||||
SERVICE="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---- Prerequisite Checks ----
|
||||
|
||||
check_prerequisites() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
error "Docker is not installed. Install it from https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker info &> /dev/null 2>&1; then
|
||||
error "Docker daemon is not running. Start Docker Desktop or the Docker service."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker compose version &> /dev/null 2>&1; then
|
||||
error "Docker Compose V2 is not available. Update Docker or install docker-compose."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
error "Environment file '$ENV_FILE' not found."
|
||||
if [ "$ENV_FILE" == ".env.docker" ]; then
|
||||
info "Creating .env.docker from default settings..."
|
||||
cat > .env.docker << 'ENVEOF'
|
||||
ENV=production
|
||||
LOG_LEVEL=INFO
|
||||
LOG_FORMAT=console
|
||||
ENABLE_REQUEST_LOGGING=true
|
||||
TRANSLATION_SERVICE=google
|
||||
GOOGLE_TRANSLATE_ENABLED=true
|
||||
DEEPL_ENABLED=false
|
||||
OPENAI_ENABLED=false
|
||||
OLLAMA_ENABLED=false
|
||||
OPENROUTER_ENABLED=false
|
||||
PROVIDER_FALLBACK_CHAIN=google,deepl,openai,ollama,openrouter
|
||||
FALLBACK_CHAIN_CLASSIC=google,deepl
|
||||
FALLBACK_CHAIN_LLM=ollama,openai
|
||||
MAX_FILE_SIZE_MB=50
|
||||
MAX_REQUEST_SIZE_MB=100
|
||||
REQUEST_TIMEOUT_SECONDS=300
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_PER_MINUTE=30
|
||||
RATE_LIMIT_PER_HOUR=200
|
||||
TRANSLATIONS_PER_MINUTE=10
|
||||
TRANSLATIONS_PER_HOUR=50
|
||||
MAX_CONCURRENT_TRANSLATIONS=5
|
||||
CLEANUP_ENABLED=true
|
||||
CLEANUP_INTERVAL_MINUTES=5
|
||||
FILE_TTL_MINUTES=60
|
||||
INPUT_FILE_TTL_MINUTES=30
|
||||
OUTPUT_FILE_TTL_MINUTES=120
|
||||
DISK_WARNING_THRESHOLD_GB=5.0
|
||||
DISK_CRITICAL_THRESHOLD_GB=1.0
|
||||
ENABLE_HSTS=false
|
||||
CORS_ORIGINS=http://localhost,http://localhost:3000,http://localhost:8000
|
||||
MAX_MEMORY_PERCENT=80
|
||||
POSTGRES_USER=translate
|
||||
POSTGRES_PASSWORD=translate_local_2026
|
||||
POSTGRES_DB=translate_db
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_ECHO=false
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
ADMIN_TOKEN_SECRET=docker_local_admin_token_secret_2026_abc123xyz
|
||||
JWT_SECRET_KEY=docker_local_jwt_secret_key_2026_x7k9m3p5q8r2s4t6
|
||||
FRONTEND_URL=http://localhost
|
||||
NEXT_PUBLIC_API_URL=
|
||||
BACKEND_URL=http://backend:8000
|
||||
HTTP_PORT=80
|
||||
ENVEOF
|
||||
success "Created .env.docker with default local settings"
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for port conflicts
|
||||
if command -v lsof &> /dev/null; then
|
||||
for PORT in 80 3000 5432 6379; do
|
||||
if lsof -i ":$PORT" -sTCP:LISTEN &> /dev/null 2>&1; then
|
||||
warn "Port $PORT is already in use. This may cause conflicts."
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
success "Prerequisites OK (Docker + Compose + $ENV_FILE)"
|
||||
}
|
||||
|
||||
# ---- Command Functions ----
|
||||
|
||||
cmd_start() {
|
||||
header "Office Translator - Starting"
|
||||
|
||||
check_prerequisites
|
||||
|
||||
if [ "$REBUILD" = true ]; then
|
||||
info "Building images (no cache)..."
|
||||
dc build --no-cache ${SERVICE:-}
|
||||
elif [ "$NO_BUILD" = false ]; then
|
||||
info "Building images..."
|
||||
dc build ${SERVICE:-}
|
||||
else
|
||||
info "Skipping build (--no-build)"
|
||||
fi
|
||||
|
||||
info "Starting services..."
|
||||
dc up -d ${SERVICE:-}
|
||||
|
||||
info "Waiting for services to be ready..."
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
while [ $elapsed -lt $max_wait ]; do
|
||||
local backend_ok=false
|
||||
local frontend_ok=false
|
||||
|
||||
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
|
||||
backend_ok=true
|
||||
fi
|
||||
|
||||
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
|
||||
frontend_ok=true
|
||||
fi
|
||||
|
||||
if [ "$backend_ok" = true ] && [ "$frontend_ok" = true ]; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
elapsed=$((elapsed + 3))
|
||||
printf "\r Waiting... (%ds/%ds) backend=%s frontend=%s" \
|
||||
"$elapsed" "$max_wait" "$backend_ok" "$frontend_ok"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Health check
|
||||
echo ""
|
||||
cmd_health
|
||||
|
||||
# Show status
|
||||
echo ""
|
||||
dc ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
echo ""
|
||||
success "Application is running!"
|
||||
echo ""
|
||||
info "Access points:"
|
||||
echo " Frontend: http://localhost"
|
||||
echo " Backend: http://localhost:8000"
|
||||
echo " API docs: http://localhost:8000/docs"
|
||||
echo " Admin: http://localhost/admin (admin / admin123)"
|
||||
echo " Health: http://localhost/health"
|
||||
echo ""
|
||||
info "Useful commands:"
|
||||
echo " ./deploy.sh logs Follow logs"
|
||||
echo " ./deploy.sh status Check status"
|
||||
echo " ./deploy.sh stop Stop all"
|
||||
}
|
||||
|
||||
cmd_stop() {
|
||||
header "Stopping Services"
|
||||
dc down --remove-orphans ${SERVICE:-}
|
||||
success "Services stopped"
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
header "Restarting Services"
|
||||
info "Stopping..."
|
||||
dc restart ${SERVICE:-}
|
||||
success "Services restarted"
|
||||
sleep 5
|
||||
cmd_health
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
header "Service Status"
|
||||
dc ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
|
||||
echo ""
|
||||
|
||||
# Quick health
|
||||
info "Health checks:"
|
||||
local backend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null || echo "000")
|
||||
local frontend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000 2>/dev/null || echo "000")
|
||||
local nginx_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/health 2>/dev/null || echo "000")
|
||||
|
||||
[ "$backend_code" = "200" ] && success "Backend (HTTP $backend_code)" || error "Backend (HTTP $backend_code)"
|
||||
[ "$frontend_code" = "200" ] && success "Frontend (HTTP $frontend_code)" || error "Frontend (HTTP $frontend_code)"
|
||||
[ "$nginx_code" = "200" ] && success "Nginx (HTTP $nginx_code)" || error "Nginx (HTTP $nginx_code)"
|
||||
}
|
||||
|
||||
cmd_logs() {
|
||||
local target="${SERVICE:-}"
|
||||
if [ -n "$target" ]; then
|
||||
dc logs -f --tail 100 "$target"
|
||||
else
|
||||
dc logs -f --tail 50
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_build() {
|
||||
header "Building Images"
|
||||
check_prerequisites
|
||||
|
||||
if [ "$REBUILD" = true ]; then
|
||||
info "Building without cache..."
|
||||
dc build --no-cache ${SERVICE:-}
|
||||
else
|
||||
info "Building..."
|
||||
dc build ${SERVICE:-}
|
||||
fi
|
||||
success "Build complete"
|
||||
}
|
||||
|
||||
cmd_health() {
|
||||
info "Running health checks..."
|
||||
|
||||
# Backend
|
||||
local backend_response=$(curl -sf http://localhost:8000/health 2>/dev/null || echo '{"status":"unreachable"}')
|
||||
if echo "$backend_response" | python3 -c "import sys,json; d=json.load(sys.stdin); exit(0 if d.get('status')=='healthy' else 1)" 2>/dev/null; then
|
||||
success "Backend: healthy"
|
||||
echo "$backend_response" | python3 -m json.tool 2>/dev/null | sed 's/^/ /'
|
||||
else
|
||||
local backend_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health 2>/dev/null || echo "000")
|
||||
error "Backend: unhealthy (HTTP $backend_code)"
|
||||
fi
|
||||
|
||||
# Frontend
|
||||
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
|
||||
success "Frontend: responding"
|
||||
else
|
||||
error "Frontend: not responding"
|
||||
fi
|
||||
|
||||
# Nginx
|
||||
if curl -sf http://localhost/health > /dev/null 2>&1; then
|
||||
success "Nginx: proxying correctly"
|
||||
else
|
||||
warn "Nginx: not reachable on port 80"
|
||||
fi
|
||||
|
||||
# PostgreSQL
|
||||
if dc exec -T postgres pg_isready -U translate -d translate_db &> /dev/null; then
|
||||
success "PostgreSQL: ready"
|
||||
else
|
||||
error "PostgreSQL: not ready"
|
||||
fi
|
||||
|
||||
# Redis
|
||||
if dc exec -T redis redis-cli ping &> /dev/null; then
|
||||
success "Redis: ready (PONG)"
|
||||
else
|
||||
error "Redis: not ready"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_backup() {
|
||||
header "Database Backup"
|
||||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
||||
local backup_dir="backups"
|
||||
mkdir -p "$backup_dir"
|
||||
|
||||
local backup_file="$backup_dir/translate_db_${timestamp}.sql.gz"
|
||||
|
||||
info "Backing up PostgreSQL to $backup_file..."
|
||||
dc exec -T postgres pg_dump -U translate translate_db | gzip > "$backup_file"
|
||||
|
||||
local size=$(du -h "$backup_file" | cut -f1)
|
||||
success "Backup complete: $backup_file ($size)"
|
||||
}
|
||||
|
||||
cmd_clean() {
|
||||
header "Full Cleanup"
|
||||
warn "This will remove all containers, volumes, and images for this project."
|
||||
read -p "Are you sure? [y/N] " -n 1 -r
|
||||
echo ""
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
info "Stopping and removing containers..."
|
||||
dc down -v --rmi local --remove-orphans
|
||||
|
||||
info "Pruning dangling resources..."
|
||||
docker system prune -f
|
||||
|
||||
success "Cleanup complete"
|
||||
else
|
||||
info "Cancelled"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_shell() {
|
||||
local target="${SERVICE:-backend}"
|
||||
info "Opening shell in $target container..."
|
||||
dc exec "$target" /bin/bash || dc exec "$target" /bin/sh
|
||||
}
|
||||
|
||||
cmd_migrate() {
|
||||
header "Running Database Migrations"
|
||||
info "Running alembic upgrade head..."
|
||||
dc exec backend alembic upgrade head
|
||||
success "Migrations complete"
|
||||
}
|
||||
|
||||
cmd_help() {
|
||||
echo ""
|
||||
echo -e "${BOLD}Office Translator - Deployment Script${NC}"
|
||||
echo ""
|
||||
echo "Usage: ./deploy.sh <command> [options]"
|
||||
echo ""
|
||||
echo -e "${BOLD}Commands:${NC}"
|
||||
echo " start Build and start all services"
|
||||
echo " stop Stop all services"
|
||||
echo " restart Restart all services"
|
||||
echo " status Show services status"
|
||||
echo " logs [svc] Show logs (optional: backend, frontend, postgres, redis, nginx)"
|
||||
echo " build Build/rebuild Docker images"
|
||||
echo " health Run health checks on all services"
|
||||
echo " backup Backup PostgreSQL database"
|
||||
echo " clean Remove all containers, volumes, and images"
|
||||
echo " shell [svc] Open shell in a container (default: backend)"
|
||||
echo " migrate Run database migrations"
|
||||
echo " help Show this help message"
|
||||
echo ""
|
||||
echo -e "${BOLD}Options:${NC}"
|
||||
echo " --env <file> Use a specific env file (default: .env.docker)"
|
||||
echo " --prod Use production compose file"
|
||||
echo " --no-build Skip build step"
|
||||
echo " --rebuild Force rebuild without Docker cache"
|
||||
echo ""
|
||||
echo -e "${BOLD}Examples:${NC}"
|
||||
echo " ./deploy.sh start"
|
||||
echo " ./deploy.sh start --rebuild"
|
||||
echo " ./deploy.sh start --prod --env .env.production"
|
||||
echo " ./deploy.sh logs backend"
|
||||
echo " ./deploy.sh shell postgres"
|
||||
echo " ./deploy.sh backup"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# ---- Main ----
|
||||
|
||||
case "$COMMAND" in
|
||||
start) cmd_start ;;
|
||||
stop) cmd_stop ;;
|
||||
restart) cmd_restart ;;
|
||||
status) cmd_status ;;
|
||||
logs) cmd_logs ;;
|
||||
build) cmd_build ;;
|
||||
health) cmd_health ;;
|
||||
backup) cmd_backup ;;
|
||||
clean) cmd_clean ;;
|
||||
shell) cmd_shell ;;
|
||||
migrate) cmd_migrate ;;
|
||||
help|--help|-h)
|
||||
cmd_help
|
||||
;;
|
||||
*)
|
||||
error "Unknown command: $COMMAND"
|
||||
cmd_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user