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

- 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:
Lefteris Notas
2026-06-12 19:32:57 +03:00
parent fa71c60cfc
commit e75f913f1c
226 changed files with 17547 additions and 0 deletions

154
scripts/backup.sh Normal file
View 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
View 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
View 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 ""