feat: complete Phase 1 foundation scaffold
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
Some checks failed
CI / Lint (push) Has been cancelled
CI / TypeCheck (push) Has been cancelled
CI / Test (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/api/Dockerfile name:api]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/cms/Dockerfile name:cms]) (push) Has been cancelled
Deploy / Docker Build & Push (map[context:. dockerfile:apps/web/Dockerfile name:web]) (push) Has been cancelled
CI / Build (push) Has been cancelled
Deploy / Deploy (push) Has been cancelled
- Monorepo: Turborepo + pnpm workspaces with 7 apps/packages - Backend: NestJS scaffold with 9 modules (auth, tenant, curriculum, simulation, portfolio, ai-coach, analytics, classroom, gamification) - Frontend: React 18 + Vite + TailwindCSS + shadcn/ui with 10 pages, 14 UI primitives, 3 Zustand stores - Database: Prisma schema with 19 models, 13 enums, seed script, multi-tenant ready - Docker: Dev, production, and Portainer Compose files with Traefik reverse proxy - Configuration: .env.example (47 vars), Zod validation, frontend-safe env exposure - i18n: English + Greek locale files (10 files), ICU MessageFormat, react-i18next - Shared packages: @investplay/types, @investplay/ui, @investplay/i18n, @investplay/utils - CI/CD: GitHub Actions (lint, typecheck, test, build, deploy, PR checks) - Documentation: CONTEXT.md, ARCHITECTURE.md (10 Mermaid diagrams), API.md, LOCALIZATION.md, DEVELOPMENT-ROADMAP.md - Infrastructure: Dockerfiles (dev + prod), nginx configs, backup/healthcheck scripts - Security: JWT auth guards, role-based access, rate limiting, Helmet, CORS, PII sanitization
This commit is contained in:
154
scripts/backup.sh
Normal file
154
scripts/backup.sh
Normal file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Configuration ──────────────────────────────────────────────
|
||||
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||||
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
||||
BACKUP_PATH="${BACKUP_DIR}/${TIMESTAMP}"
|
||||
RETENTION_DAILY=7
|
||||
RETENTION_WEEKLY=4
|
||||
|
||||
# S3-compatible backup bucket (optional)
|
||||
S3_ENDPOINT="${S3_BACKUP_ENDPOINT:-}"
|
||||
S3_BUCKET="${S3_BACKUP_BUCKET:-}"
|
||||
S3_ACCESS_KEY="${S3_BACKUP_ACCESS_KEY:-}"
|
||||
S3_SECRET_KEY="${S3_BACKUP_SECRET_KEY:-}"
|
||||
|
||||
# ─── Color output ──────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
cleanup() {
|
||||
local exit_code=$?
|
||||
if [ $exit_code -ne 0 ]; then
|
||||
error "Backup script failed with exit code ${exit_code}"
|
||||
fi
|
||||
exit $exit_code
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# ─── Ensure backup directory ───────────────────────────────────
|
||||
mkdir -p "${BACKUP_PATH}"
|
||||
|
||||
# ─── 1. PostgreSQL backup ───────────────────────────────────────
|
||||
info "Backing up PostgreSQL..."
|
||||
POSTGRES_CONTAINER="${POSTGRES_CONTAINER:-investplay-postgres}"
|
||||
POSTGRES_USER="${POSTGRES_USER:-investplay}"
|
||||
POSTGRES_DB="${POSTGRES_DB:-investplay}"
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${POSTGRES_CONTAINER}$"; then
|
||||
docker exec "${POSTGRES_CONTAINER}" pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" \
|
||||
--no-owner --no-acl --compress=9 \
|
||||
> "${BACKUP_PATH}/postgres_${TIMESTAMP}.sql.gz" 2>/dev/null || {
|
||||
warn "pg_dump failed — database may be temporarily unavailable"
|
||||
}
|
||||
ok "PostgreSQL backup saved"
|
||||
else
|
||||
warn "PostgreSQL container '${POSTGRES_CONTAINER}' not running — skipping"
|
||||
fi
|
||||
|
||||
# ─── 2. Redis backup ────────────────────────────────────────────
|
||||
info "Backing up Redis..."
|
||||
REDIS_CONTAINER="${REDIS_CONTAINER:-investplay-redis}"
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${REDIS_CONTAINER}$"; then
|
||||
docker exec "${REDIS_CONTAINER}" redis-cli SAVE >/dev/null 2>&1 || {
|
||||
warn "Redis SAVE command failed"
|
||||
}
|
||||
# Copy the dump file from the container
|
||||
docker cp "${REDIS_CONTAINER}:/data/dump.rdb" "${BACKUP_PATH}/redis_${TIMESTAMP}.rdb" 2>/dev/null || {
|
||||
warn "Failed to copy Redis dump"
|
||||
}
|
||||
ok "Redis backup saved"
|
||||
else
|
||||
warn "Redis container '${REDIS_CONTAINER}' not running — skipping"
|
||||
fi
|
||||
|
||||
# ─── 3. MinIO backup ────────────────────────────────────────────
|
||||
info "Backing up MinIO..."
|
||||
MINIO_CONTAINER="${MINIO_CONTAINER:-investplay-minio}"
|
||||
MINIO_ALIAS="${MINIO_ALIAS:-investplay}"
|
||||
MINIO_USER="${MINIO_ROOT_USER:-minioadmin}"
|
||||
MINIO_PASS="${MINIO_ROOT_PASSWORD:-}"
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${MINIO_CONTAINER}$"; then
|
||||
# Configure mc alias inside the container
|
||||
docker exec "${MINIO_CONTAINER}" mc alias set "${MINIO_ALIAS}" \
|
||||
http://localhost:9000 "${MINIO_USER}" "${MINIO_PASS}" >/dev/null 2>&1
|
||||
|
||||
# Mirror all buckets to local backup directory
|
||||
mkdir -p "${BACKUP_PATH}/minio"
|
||||
docker exec "${MINIO_CONTAINER}" mc mirror --overwrite \
|
||||
"${MINIO_ALIAS}/" "/tmp/minio-backup" >/dev/null 2>&1 || {
|
||||
warn "MinIO mc mirror failed"
|
||||
}
|
||||
docker cp "${MINIO_CONTAINER}:/tmp/minio-backup/." "${BACKUP_PATH}/minio/" 2>/dev/null || true
|
||||
docker exec "${MINIO_CONTAINER}" rm -rf /tmp/minio-backup 2>/dev/null || true
|
||||
ok "MinIO backup saved"
|
||||
else
|
||||
warn "MinIO container '${MINIO_CONTAINER}' not running — skipping"
|
||||
fi
|
||||
|
||||
# ─── 4. Upload to S3-compatible backup bucket (optional) ────────
|
||||
if [ -n "${S3_ENDPOINT}" ] && [ -n "${S3_BUCKET}" ]; then
|
||||
info "Uploading backup to S3-compatible storage..."
|
||||
|
||||
if command -v mc &>/dev/null; then
|
||||
mc alias set backup-target "${S3_ENDPOINT}" "${S3_ACCESS_KEY}" "${S3_SECRET_KEY}" >/dev/null 2>&1
|
||||
mc mirror "${BACKUP_PATH}" "backup-target/${S3_BUCKET}/investplay/${TIMESTAMP}" >/dev/null 2>&1 && {
|
||||
ok "Backup uploaded to S3"
|
||||
} || {
|
||||
warn "S3 upload failed"
|
||||
}
|
||||
elif command -v aws &>/dev/null; then
|
||||
AWS_ACCESS_KEY_ID="${S3_ACCESS_KEY}" AWS_SECRET_ACCESS_KEY="${S3_SECRET_KEY}" \
|
||||
aws s3 cp --endpoint-url="${S3_ENDPOINT}" --recursive \
|
||||
"${BACKUP_PATH}" "s3://${S3_BUCKET}/investplay/${TIMESTAMP}/" >/dev/null 2>&1 && {
|
||||
ok "Backup uploaded to S3"
|
||||
} || {
|
||||
warn "S3 upload failed"
|
||||
}
|
||||
else
|
||||
warn "Neither 'mc' nor 'aws' CLI found — skipping S3 upload"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ─── 5. Cleanup old backups ─────────────────────────────────────
|
||||
info "Cleaning up old backups..."
|
||||
total_backups=$(ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | wc -l)
|
||||
|
||||
# Keep last N daily backups
|
||||
ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | sort -r | tail -n +$((RETENTION_DAILY + 1)) | while read -r dir; do
|
||||
rm -rf "${dir}"
|
||||
info "Removed old backup: $(basename "${dir}")"
|
||||
done
|
||||
|
||||
# Keep last N weekly backups (every Sunday)
|
||||
if [ "$(date +%u)" -eq 7 ]; then
|
||||
ls -1d "${BACKUP_DIR}/"20*/ 2>/dev/null | sort -r | tail -n +$((RETENTION_WEEKLY + 1)) | while read -r dir; do
|
||||
rm -rf "${dir}"
|
||||
info "Removed weekly backup: $(basename "${dir}")"
|
||||
done
|
||||
fi
|
||||
|
||||
ok "Cleanup complete"
|
||||
|
||||
# ─── Summary ────────────────────────────────────────────────────
|
||||
backup_size=$(du -sh "${BACKUP_PATH}" | cut -f1)
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} Backup completed successfully${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}Location:${NC} ${BACKUP_PATH}"
|
||||
echo -e " ${CYAN}Size:${NC} ${backup_size}"
|
||||
echo -e " ${CYAN}Date:${NC} $(date)"
|
||||
echo ""
|
||||
121
scripts/healthcheck.sh
Normal file
121
scripts/healthcheck.sh
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Color output ──────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
header() { echo -e "\n${YELLOW}─── $* ───${NC}"; }
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
# ─── Docker service health ──────────────────────────────────────
|
||||
header "Docker Container Status"
|
||||
|
||||
containers=$(docker ps --format "{{.Names}}" 2>/dev/null || true)
|
||||
if [ -z "${containers}" ]; then
|
||||
warn "No running Docker containers found"
|
||||
else
|
||||
while IFS= read -r name; do
|
||||
status=$(docker inspect --format='{{.State.Status}}' "${name}" 2>/dev/null || echo "unknown")
|
||||
health=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "${name}" 2>/dev/null || echo "none")
|
||||
|
||||
if [ "${status}" = "running" ]; then
|
||||
if [ "${health}" = "healthy" ]; then
|
||||
ok "${name} — running, healthy"
|
||||
elif [ "${health}" = "unhealthy" ]; then
|
||||
error "${name} — running but UNHEALTHY"
|
||||
EXIT_CODE=1
|
||||
else
|
||||
warn "${name} — running (no health check)"
|
||||
fi
|
||||
else
|
||||
error "${name} — ${status}"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
done <<< "${containers}"
|
||||
fi
|
||||
|
||||
# ─── HTTP health endpoints ───────────────────────────────────────
|
||||
header "HTTP Health Endpoints"
|
||||
|
||||
check_endpoint() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected_code="${3:-200}"
|
||||
|
||||
if command -v curl &>/dev/null; then
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "${url}" 2>/dev/null || echo "000")
|
||||
elif command -v wget &>/dev/null; then
|
||||
http_code=$(wget --server-response --spider --timeout=5 "${url}" 2>&1 | awk '/HTTP\/1.1/{print $2}' | tail -1 || echo "000")
|
||||
else
|
||||
warn "Neither curl nor wget available — skipping HTTP checks"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "${http_code}" = "${expected_code}" ] || [ "${http_code}" = "000" ]; then
|
||||
if [ "${http_code}" = "000" ]; then
|
||||
warn "${name} — could not connect to ${url}"
|
||||
else
|
||||
ok "${name} — ${url} returned ${http_code}"
|
||||
fi
|
||||
else
|
||||
error "${name} — ${url} returned ${http_code} (expected ${expected_code})"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
}
|
||||
|
||||
check_endpoint "API" "http://localhost:3001/health"
|
||||
check_endpoint "Web" "http://localhost:80/"
|
||||
check_endpoint "CMS" "http://localhost:3000/health"
|
||||
check_endpoint "MinIO (API)" "http://localhost:9000/minio/health/live"
|
||||
check_endpoint "MinIO (Console)" "http://localhost:9001/"
|
||||
|
||||
# ─── Port check ─────────────────────────────────────────────────
|
||||
header "Port Availability"
|
||||
|
||||
check_port() {
|
||||
local name="$1"
|
||||
local port="$2"
|
||||
|
||||
if command -v nc &>/dev/null; then
|
||||
if nc -z localhost "${port}" 2>/dev/null; then
|
||||
ok "${name} — port ${port} is open"
|
||||
else
|
||||
error "${name} — port ${port} is CLOSED"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
else
|
||||
# Fallback: try connecting with /dev/tcp (bash built-in)
|
||||
if timeout 1 bash -c "echo >/dev/tcp/localhost/${port}" 2>/dev/null; then
|
||||
ok "${name} — port ${port} is open"
|
||||
else
|
||||
error "${name} — port ${port} is CLOSED"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
check_port "PostgreSQL" 5432
|
||||
check_port "Redis" 6379
|
||||
|
||||
# ─── Summary ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "${EXIT_CODE}" -eq 0 ]; then
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} All services are healthy!${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
else
|
||||
echo -e "${RED}══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${RED} Some services have issues (exit code: ${EXIT_CODE})${NC}"
|
||||
echo -e "${RED}══════════════════════════════════════════════════════════${NC}"
|
||||
fi
|
||||
|
||||
exit "${EXIT_CODE}"
|
||||
127
scripts/setup.sh
Normal file
127
scripts/setup.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ─── Color output ──────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
|
||||
# ─── Prerequisites ──────────────────────────────────────────────
|
||||
info "Checking prerequisites..."
|
||||
|
||||
command -v docker >/dev/null 2>&1 || { error "Docker is required but not installed."; exit 1; }
|
||||
ok "Docker $(docker --version)"
|
||||
|
||||
docker compose version >/dev/null 2>&1 || { error "Docker Compose is required but not installed."; exit 1; }
|
||||
ok "Docker Compose $(docker compose version --short)"
|
||||
|
||||
command -v node >/dev/null 2>&1 || { error "Node.js is required but not installed."; exit 1; }
|
||||
ok "Node.js $(node --version)"
|
||||
|
||||
command -v pnpm >/dev/null 2>&1 || { error "pnpm is required but not installed. Install it: npm install -g pnpm"; exit 1; }
|
||||
ok "pnpm $(pnpm --version)"
|
||||
|
||||
# ─── Project root ───────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${PROJECT_ROOT}"
|
||||
|
||||
# ─── Environment file ───────────────────────────────────────────
|
||||
if [ ! -f .env ]; then
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
info "Created .env from .env.example — please review and update credentials."
|
||||
else
|
||||
warn "No .env.example found. Creating minimal .env..."
|
||||
cat > .env <<-EOF
|
||||
POSTGRES_USER=investplay
|
||||
POSTGRES_PASSWORD=investplay_dev_pass
|
||||
POSTGRES_DB=investplay
|
||||
DATABASE_URL=postgresql://investplay:investplay_dev_pass@localhost:5432/investplay
|
||||
REDIS_URL=redis://localhost:6379
|
||||
MINIO_ROOT_USER=minioadmin
|
||||
MINIO_ROOT_PASSWORD=minioadmin_dev
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
JWT_SECRET=change-me-in-production
|
||||
EOF
|
||||
ok "Created minimal .env"
|
||||
fi
|
||||
else
|
||||
ok ".env already exists"
|
||||
fi
|
||||
|
||||
# ─── Start infrastructure services ──────────────────────────────
|
||||
info "Starting infrastructure (PostgreSQL, Redis, MinIO)..."
|
||||
docker compose -f docker-compose.dev.yml up -d postgres redis minio
|
||||
|
||||
info "Waiting for PostgreSQL to become healthy..."
|
||||
until docker compose -f docker-compose.dev.yml exec -T postgres pg_isready -U "${POSTGRES_USER:-investplay}" 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
ok "PostgreSQL is ready"
|
||||
|
||||
info "Waiting for Redis to become healthy..."
|
||||
until docker compose -f docker-compose.dev.yml exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; do
|
||||
sleep 1
|
||||
done
|
||||
ok "Redis is ready"
|
||||
|
||||
info "Waiting for MinIO to become healthy..."
|
||||
until docker compose -f docker-compose.dev.yml exec -T minio mc ready local 2>/dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
ok "MinIO is ready"
|
||||
|
||||
# ─── Install dependencies ───────────────────────────────────────
|
||||
info "Installing pnpm dependencies..."
|
||||
pnpm install
|
||||
ok "Dependencies installed"
|
||||
|
||||
# ─── Generate Prisma client ─────────────────────────────────────
|
||||
info "Running Prisma migrations..."
|
||||
if [ -f apps/api/prisma/schema.prisma ]; then
|
||||
pnpm --filter @investplay/api exec prisma generate
|
||||
pnpm --filter @investplay/api exec prisma migrate dev --name init 2>/dev/null || true
|
||||
ok "Prisma client generated and migrations applied"
|
||||
else
|
||||
warn "No Prisma schema found at apps/api/prisma/schema.prisma — skipping migrations"
|
||||
fi
|
||||
|
||||
# ─── Seed database ─────────────────────────────────────────────
|
||||
if [ -f apps/api/prisma/seed.ts ] || [ -f apps/api/prisma/seed.js ]; then
|
||||
info "Seeding database..."
|
||||
pnpm --filter @investplay/api exec prisma db seed 2>/dev/null || true
|
||||
ok "Database seeded"
|
||||
else
|
||||
info "No seed script found — skipping"
|
||||
fi
|
||||
|
||||
# ─── Start remaining services ──────────────────────────────────
|
||||
info "Starting API, Web, and CMS..."
|
||||
docker compose -f docker-compose.dev.yml up -d api web cms
|
||||
ok "All services started"
|
||||
|
||||
# ─── Summary ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${GREEN} InvestPlay development environment is ready!${NC}"
|
||||
echo -e "${GREEN}══════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
echo -e " ${CYAN}Web App:${NC} http://localhost:5173"
|
||||
echo -e " ${CYAN}API:${NC} http://localhost:3001"
|
||||
echo -e " ${CYAN}CMS:${NC} http://localhost:3000"
|
||||
echo -e " ${CYAN}MinIO Console:${NC} http://localhost:9001"
|
||||
echo -e " ${CYAN}PostgreSQL:${NC} localhost:5432"
|
||||
echo -e " ${CYAN}Redis:${NC} localhost:6379"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Run 'docker compose -f docker-compose.dev.yml logs -f' to tail logs${NC}"
|
||||
echo ""
|
||||
Reference in New Issue
Block a user