diff --git a/.env.example b/.env.example index 9d2e778..c44fdb3 100644 --- a/.env.example +++ b/.env.example @@ -79,4 +79,4 @@ TEST_PASSWORD=Test123!@# PANDASCORE_API_KEY=your_pandascore_api_key_here PANDASCORE_BASE_URL=https://api.pandascore.co -PANDASCORE_CACHE_TTL=3600 \ No newline at end of file +PANDASCORE_CACHE_TTL=3600 diff --git a/.env.production.example b/.env.production.example index f7be876..31d40b9 100644 --- a/.env.production.example +++ b/.env.production.example @@ -27,7 +27,7 @@ SECRET_KEY_BASE=CHANGE_ME_GENERATE_WITH_rails_secret DEVISE_JWT_SECRET_KEY=CHANGE_ME_GENERATE_WITH_rails_secret # CORS -CORS_ORIGINS=https://prostaff.gg,https://api.prostaff.gg,https://www.prostaff.gg +CORS_ORIGINS=https://prostaff.gg,https://www.prostaff.gg,https://prostaffgg.netlify.app # External APIs RIOT_API_KEY=RGAPI-YOUR-PRODUCTION-KEY-HERE diff --git a/.gitignore b/.gitignore index 35ffed8..f856ea0 100644 --- a/.gitignore +++ b/.gitignore @@ -252,3 +252,9 @@ TEST_ANALYSIS_REPORT.md MODULAR_MIGRATION_PHASE1_SUMMARY.md MODULAR_MONOLITH_MIGRATION_PLAN.md app/modules/players/README.md +/League-Data-Scraping-And-Analytics-master/jsons +/League-Data-Scraping-And-Analytics-master/Pro/game +/League-Data-Scraping-And-Analytics-master/Pro/timeline +League-Data-Scraping-And-Analytics-master/ProStaff-Scraper/ +DOCS/ELASTICSEARCH_SETUP.md +DOCS/deployment/QUICK_DEPLOY_VPS.md diff --git a/Gemfile b/Gemfile index a32a918..516c9c1 100644 --- a/Gemfile +++ b/Gemfile @@ -75,6 +75,9 @@ gem 'rswag' gem 'rswag-api' gem 'rswag-ui' +# Elasticsearch client (for analytics queries) +gem 'elasticsearch', '~> 9.1', '>= 9.1.3' + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem 'debug', platforms: %i[mri mingw x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 9898e0c..6f85b47 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -107,6 +107,14 @@ GEM dotenv (= 3.1.8) railties (>= 6.1) drb (2.2.3) + elastic-transport (8.4.1) + faraday (< 3) + multi_json + elasticsearch (9.2.0) + elastic-transport (~> 8.3) + elasticsearch-api (= 9.2.0) + elasticsearch-api (9.2.0) + multi_json erb (5.0.3) erubi (1.13.1) et-orbi (1.4.0) @@ -172,6 +180,7 @@ GEM mini_mime (1.1.5) minitest (5.26.0) msgpack (1.8.0) + multi_json (1.18.0) net-http (0.6.0) uri net-imap (0.5.12) @@ -371,6 +380,7 @@ DEPENDENCIES database_cleaner-active_record debug dotenv-rails + elasticsearch (~> 9.1, >= 9.1.3) factory_bot_rails faker faraday diff --git a/app/controllers/api/v1/analytics/performance_controller.rb b/app/controllers/api/v1/analytics/performance_controller.rb index a3d6d8a..98df18c 100644 --- a/app/controllers/api/v1/analytics/performance_controller.rb +++ b/app/controllers/api/v1/analytics/performance_controller.rb @@ -75,12 +75,9 @@ def apply_date_filters(matches) # @param period [String] Time period (week, month, season) # @return [Integer] Number of days def time_period_to_days(period) - case period - when 'week' then 7 - when 'month' then 30 - when 'season' then 90 - else 30 - end + return 7 if period == 'week' + return 90 if period == 'season' + 30 end # Legacy method - kept for backwards compatibility diff --git a/app/controllers/api/v1/dashboard_controller_optimized.rb b/app/controllers/api/v1/dashboard_controller_optimized.rb index 7a5166d..079b739 100644 --- a/app/controllers/api/v1/dashboard_controller_optimized.rb +++ b/app/controllers/api/v1/dashboard_controller_optimized.rb @@ -56,8 +56,8 @@ def calculate_win_rate_fast(wins, total) ((wins.to_f / total) * 100).round(1) end - def calculate_recent_form(matches) - matches.map { |m| m.victory? ? 'W' : 'L' }.join('') + def calculate_recent_form(matches) + matches.map { |m| m.victory? ? 'W' : 'L' }.join end def calculate_average_kda_fast(kda_result) diff --git a/app/controllers/api/v1/scrims/opponent_teams_controller.rb b/app/controllers/api/v1/scrims/opponent_teams_controller.rb index 143db6b..9fde5d1 100644 --- a/app/controllers/api/v1/scrims/opponent_teams_controller.rb +++ b/app/controllers/api/v1/scrims/opponent_teams_controller.rb @@ -113,16 +113,13 @@ def destroy # only modify teams they have scrims with. # Read operations (index/show) are allowed for all teams to enable discovery. # - # SECURITY: Unscoped find is intentional here. OpponentTeam is a global - # resource visible to all organizations for discovery. Authorization is - # handled by verify_team_usage! for modifications. - # rubocop:disable Rails/FindById def set_opponent_team - @opponent_team = OpponentTeam.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render json: { error: 'Opponent team not found' }, status: :not_found + id = Integer(params[:id]) rescue nil + return render json: { error: 'Opponent team not found' }, status: :not_found unless id + + @opponent_team = OpponentTeam.find_by(id: id) + return render json: { error: 'Opponent team not found' }, status: :not_found unless @opponent_team end - # rubocop:enable Rails/FindById # Verifies that current organization has used this opponent team # Prevents organizations from modifying/deleting teams they haven't interacted with diff --git a/app/jobs/sync_player_from_riot_job.rb b/app/jobs/sync_player_from_riot_job.rb index 11ed458..42071cb 100644 --- a/app/jobs/sync_player_from_riot_job.rb +++ b/app/jobs/sync_player_from_riot_job.rb @@ -6,69 +6,26 @@ class SyncPlayerFromRiotJob < ApplicationJob def perform(player_id) player = Player.find(player_id) - unless player.riot_puuid.present? || player.summoner_name.present? - player.update(sync_status: 'error', last_sync_at: Time.current) - Rails.logger.error "Player #{player_id} missing Riot info" - return - end + return mark_error(player, "Player #{player_id} missing Riot info") unless player.riot_puuid.present? || player.summoner_name.present? riot_api_key = ENV['RIOT_API_KEY'] - unless riot_api_key.present? - player.update(sync_status: 'error', last_sync_at: Time.current) - Rails.logger.error 'Riot API key not configured' - return - end + return mark_error(player, 'Riot API key not configured') unless riot_api_key.present? + + region = player.region.presence&.downcase || 'br1' begin - region = player.region.presence&.downcase || 'br1' - - summoner_data = if player.riot_puuid.present? - fetch_summoner_by_puuid(player.riot_puuid, region, riot_api_key) - else - fetch_summoner_by_name(player.summoner_name, region, riot_api_key) - end - - # Use PUUID for league endpoint (workaround for Riot API bug where summoner_data['id'] is nil) - # See: https://github.com/RiotGames/developer-relations/issues/1092 - ranked_data = fetch_ranked_stats_by_puuid(player.riot_puuid, region, riot_api_key) - - update_data = { - riot_puuid: summoner_data['puuid'], - riot_summoner_id: summoner_data['id'], - summoner_level: summoner_data['summonerLevel'], - profile_icon_id: summoner_data['profileIconId'], - sync_status: 'success', - last_sync_at: Time.current - } - - solo_queue = ranked_data.find { |q| q['queueType'] == 'RANKED_SOLO_5x5' } - if solo_queue - update_data.merge!({ - solo_queue_tier: solo_queue['tier'], - solo_queue_rank: solo_queue['rank'], - solo_queue_lp: solo_queue['leaguePoints'], - solo_queue_wins: solo_queue['wins'], - solo_queue_losses: solo_queue['losses'] - }) - end - - flex_queue = ranked_data.find { |q| q['queueType'] == 'RANKED_FLEX_SR' } - if flex_queue - update_data.merge!({ - flex_queue_tier: flex_queue['tier'], - flex_queue_rank: flex_queue['rank'], - flex_queue_lp: flex_queue['leaguePoints'] - }) - end + summoner_data = fetch_summoner(player, region, riot_api_key) + ranked_data = fetch_ranked_stats_by_puuid(summoner_data['puuid'], region, riot_api_key) - player.update!(update_data) + update_data = build_update_data(summoner_data) + update_data.merge!(extract_queue_updates(ranked_data)) + player.update!(update_data) Rails.logger.info "Successfully synced player #{player_id} from Riot API" rescue StandardError => e Rails.logger.error "Failed to sync player #{player_id}: #{e.message}" Rails.logger.error e.backtrace.join("\n") - - player.update(sync_status: 'error', last_sync_at: Time.current) + mark_error(player) end end @@ -154,3 +111,49 @@ def fetch_ranked_stats_by_puuid(puuid, region, api_key) JSON.parse(response.body) end end + def fetch_summoner(player, region, api_key) + return fetch_summoner_by_puuid(player.riot_puuid, region, api_key) if player.riot_puuid.present? + fetch_summoner_by_name(player.summoner_name, region, api_key) + end + + def build_update_data(summoner_data) + { + riot_puuid: summoner_data['puuid'], + riot_summoner_id: summoner_data['id'], + summoner_level: summoner_data['summonerLevel'], + profile_icon_id: summoner_data['profileIconId'], + sync_status: 'success', + last_sync_at: Time.current + } + end + + def extract_queue_updates(ranked_data) + updates = {} + + solo = ranked_data.find { |q| q['queueType'] == 'RANKED_SOLO_5x5' } + if solo + updates.merge!({ + solo_queue_tier: solo['tier'], + solo_queue_rank: solo['rank'], + solo_queue_lp: solo['leaguePoints'], + solo_queue_wins: solo['wins'], + solo_queue_losses: solo['losses'] + }) + end + + flex = ranked_data.find { |q| q['queueType'] == 'RANKED_FLEX_SR' } + if flex + updates.merge!({ + flex_queue_tier: flex['tier'], + flex_queue_rank: flex['rank'], + flex_queue_lp: flex['leaguePoints'] + }) + end + + updates + end + + def mark_error(player, message = nil) + Rails.logger.error(message) if message + player.update(sync_status: 'error', last_sync_at: Time.current) + end diff --git a/app/models/audit_log.rb b/app/models/audit_log.rb index 5b8b196..7f8c133 100644 --- a/app/models/audit_log.rb +++ b/app/models/audit_log.rb @@ -122,13 +122,10 @@ def time_ago end def risk_level - case action - when 'delete' then 'high' - when 'update' then 'medium' - when 'create' then 'low' - when 'login', 'logout' then 'info' - else 'medium' - end + return 'high' if action == 'delete' + return 'low' if action == 'create' + return 'info' if %w[login logout].include?(action) + 'medium' end def risk_color diff --git a/app/models/schedule.rb b/app/models/schedule.rb index 99d6da5..7d8daaa 100644 --- a/app/models/schedule.rb +++ b/app/models/schedule.rb @@ -151,7 +151,7 @@ def participant_overlap?(other) our_participants = required_players + optional_players other_participants = other.required_players + other.optional_players - (our_participants & other_participants).any? + our_participants.intersect?(other_participants) end def log_audit_trail diff --git a/app/models/team_goal.rb b/app/models/team_goal.rb index c752575..389284e 100644 --- a/app/models/team_goal.rb +++ b/app/models/team_goal.rb @@ -188,7 +188,8 @@ def update_progress!(new_current_value) end def assigned_to_name - assigned_to&.full_name || assigned_to&.email&.split('@')&.first || 'Unassigned' + return 'Unassigned' unless assigned_to + assigned_to.full_name || (assigned_to.email&.split('@')&.first) || 'Unassigned' end def player_name diff --git a/app/models/vod_review.rb b/app/models/vod_review.rb index 842c878..402877e 100644 --- a/app/models/vod_review.rb +++ b/app/models/vod_review.rb @@ -75,12 +75,9 @@ def duration_formatted end def status_color - case status - when 'draft' then 'yellow' - when 'published' then 'green' - when 'archived' then 'gray' - else 'gray' - end + return 'yellow' if status == 'draft' + return 'green' if status == 'published' + 'gray' end def can_be_edited_by?(user) diff --git a/app/models/vod_timestamp.rb b/app/models/vod_timestamp.rb index eeacfe8..dd28c44 100644 --- a/app/models/vod_timestamp.rb +++ b/app/models/vod_timestamp.rb @@ -38,13 +38,10 @@ def timestamp_formatted end def importance_color - case importance - when 'low' then 'gray' - when 'normal' then 'blue' - when 'high' then 'orange' - when 'critical' then 'red' - else 'gray' - end + return 'blue' if importance == 'normal' + return 'orange' if importance == 'high' + return 'red' if importance == 'critical' + 'gray' end def category_color diff --git a/app/modules/analytics/services/elasticsearch_client.rb b/app/modules/analytics/services/elasticsearch_client.rb new file mode 100644 index 0000000..64c0b9b --- /dev/null +++ b/app/modules/analytics/services/elasticsearch_client.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Analytics + module Services + class ElasticsearchClient + def initialize(url: ENV.fetch('ELASTICSEARCH_URL', 'http://localhost:9200')) + @client = Elasticsearch::Client.new(url: url) + end + + def ping + @client.ping + rescue StandardError => e + Rails.logger.error("Elasticsearch ping failed: #{e.message}") + false + end + + def search(index:, body: {}) + @client.search(index: index, body: body) + end + end + end +end \ No newline at end of file diff --git a/app/modules/matches/jobs/sync_match_job.rb b/app/modules/matches/jobs/sync_match_job.rb index 35468c6..b8bc709 100644 --- a/app/modules/matches/jobs/sync_match_job.rb +++ b/app/modules/matches/jobs/sync_match_job.rb @@ -81,11 +81,7 @@ def create_player_match_stats(match, participants, organization) end def determine_match_type(game_mode) - case game_mode.upcase - when 'CLASSIC' then 'official' - when 'ARAM' then 'scrim' - else 'scrim' - end + game_mode.to_s.upcase == 'CLASSIC' ? 'official' : 'scrim' end def determine_team_victory(participants, organization) diff --git a/app/modules/scrims/controllers/opponent_teams_controller.rb b/app/modules/scrims/controllers/opponent_teams_controller.rb index 143db6b..9fde5d1 100644 --- a/app/modules/scrims/controllers/opponent_teams_controller.rb +++ b/app/modules/scrims/controllers/opponent_teams_controller.rb @@ -113,16 +113,13 @@ def destroy # only modify teams they have scrims with. # Read operations (index/show) are allowed for all teams to enable discovery. # - # SECURITY: Unscoped find is intentional here. OpponentTeam is a global - # resource visible to all organizations for discovery. Authorization is - # handled by verify_team_usage! for modifications. - # rubocop:disable Rails/FindById def set_opponent_team - @opponent_team = OpponentTeam.find(params[:id]) - rescue ActiveRecord::RecordNotFound - render json: { error: 'Opponent team not found' }, status: :not_found + id = Integer(params[:id]) rescue nil + return render json: { error: 'Opponent team not found' }, status: :not_found unless id + + @opponent_team = OpponentTeam.find_by(id: id) + return render json: { error: 'Opponent team not found' }, status: :not_found unless @opponent_team end - # rubocop:enable Rails/FindById # Verifies that current organization has used this opponent team # Prevents organizations from modifying/deleting teams they haven't interacted with diff --git a/deploy/nginx/conf.d/prostaff.conf b/deploy/nginx/conf.d/prostaff.conf index 672fc74..4b72787 100644 --- a/deploy/nginx/conf.d/prostaff.conf +++ b/deploy/nginx/conf.d/prostaff.conf @@ -4,7 +4,7 @@ server { listen 80; listen [::]:80; - server_name api.prostaff.gg staging-api.prostaff.gg; + server_name api.prostaff.gg staging-api.prostaff.gg prostaff.gg www.prostaff.gg; # Health check endpoint (HTTP OK) location /health { @@ -13,8 +13,11 @@ server { add_header Content-Type text/plain; } - # Redirect all other traffic to HTTPS + # Redirect all other traffic to HTTPS (canonicalize root domain for www) location / { + if ($host = 'www.prostaff.gg') { + return 301 https://prostaff.gg$request_uri; + } return 301 https://$server_name$request_uri; } } @@ -105,6 +108,107 @@ server { } } +## HTTPS - Production (root domain) +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name prostaff.gg; + + # SSL Configuration + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # HSTS + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + # Logs + access_log /var/log/nginx/prostaff-root-access.log main; + error_log /var/log/nginx/prostaff-root-error.log warn; + + # Root directory + root /app/public; + + # Rate limiting + limit_req zone=api burst=50 nodelay; + + # Serve static files directly + location ~ ^/(assets|packs|images|javascripts|stylesheets|system)/ { + gzip_static on; + expires max; + add_header Cache-Control public; + access_log off; + try_files $uri @app; + } + + # Health check + location /up { + proxy_pass http://prostaff_api; + proxy_set_header Host $host; + access_log off; + } + + # API Documentation (Swagger) + location /api-docs { + proxy_pass http://prostaff_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy to Rails app + location / { + proxy_pass http://prostaff_api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + + # Timeouts + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Buffering + proxy_buffering on; + proxy_buffer_size 4k; + proxy_buffers 8 4k; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Error pages + error_page 500 502 503 504 /500.html; + location = /500.html { + root /app/public; + internal; + } +} + +## HTTPS - Redirect www to root domain +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name www.prostaff.gg; + + # SSL Configuration (certificate should include both root and www SANs) + ssl_certificate /etc/nginx/ssl/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + return 301 https://prostaff.gg$request_uri; +} + # HTTPS - Staging server { listen 443 ssl http2; diff --git a/docker-compose.production.yml b/docker-compose.production.yml index 988b80e..c97d083 100644 --- a/docker-compose.production.yml +++ b/docker-compose.production.yml @@ -1,6 +1,40 @@ -version: '3.8' - services: + # Elasticsearch for analytics search + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 + container_name: prostaff-elasticsearch + restart: unless-stopped + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - ES_JAVA_OPTS=-Xms1g -Xmx1g + volumes: + - es_data:/usr/share/elasticsearch/data + ports: + - "9200:9200" + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:9200 >/dev/null || exit 1"] + interval: 10s + timeout: 5s + retries: 10 + networks: + - prostaff-net + + # Kibana for exploring ES indices + kibana: + image: docker.elastic.co/kibana/kibana:8.13.4 + container_name: prostaff-kibana + restart: unless-stopped + environment: + - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 + depends_on: + elasticsearch: + condition: service_healthy + ports: + - "5601:5601" + networks: + - prostaff-net + # Nginx Reverse Proxy nginx: image: nginx:alpine @@ -17,6 +51,7 @@ services: - nginx_logs:/var/log/nginx depends_on: - api + - kibana networks: - prostaff-net healthcheck: @@ -156,6 +191,24 @@ services: memory: 256M cpus: '0.1' + # Scraper service (optional runner) + scraper: + build: + context: ./League-Data-Scraping-And-Analytics-master/ProStaff-Scraper + container_name: prostaff-scraper + env_file: + - ./League-Data-Scraping-And-Analytics-master/ProStaff-Scraper/.env + volumes: + - ./League-Data-Scraping-And-Analytics-master/ProStaff-Scraper:/app + depends_on: + elasticsearch: + condition: service_healthy + command: ["tail", "-f", "/dev/null"] + networks: + - prostaff-net + profiles: + - scraper + # Backup Service (runs daily) backup: image: postgres:15-alpine @@ -182,6 +235,8 @@ volumes: driver: local nginx_logs: driver: local + es_data: + driver: local networks: prostaff-net: diff --git a/docker-compose.yml b/docker-compose.yml index a9acbff..b5683b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,79 +1,79 @@ -services: - # PostgreSQL Database - postgres: - image: postgres:15-alpine - environment: - POSTGRES_DB: ${POSTGRES_DB:-prostaff_api_development} - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} - volumes: - - postgres_data:/var/lib/postgresql/data - ports: - - "${POSTGRES_PORT:-5432}:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis for caching and Sidekiq - redis: - image: redis:7-alpine - volumes: - - redis_data:/data - ports: - - "${REDIS_PORT:-6379}:6379" - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 5s - retries: 5 - - # Rails API - api: - build: . - container_name: prostaff-api - env_file: - - .env - volumes: - - .:/app - - bundle_cache:/usr/local/bundle - ports: - - "${API_PORT:-3333}:3000" - depends_on: - redis: - condition: service_healthy - networks: - - default - - security-net - command: > - sh -c " - bundle check || bundle install && - rm -f tmp/pids/server.pid && - bundle exec rails db:migrate && - bundle exec rails server -b 0.0.0.0 -p 3000 - " - - # Sidekiq for background jobs - sidekiq: - build: . - env_file: - - .env - volumes: - - .:/app - - bundle_cache:/usr/local/bundle - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - command: bundle exec sidekiq -C config/sidekiq.yml - -volumes: - postgres_data: - redis_data: - bundle_cache: - -networks: - security-net: - driver: bridge \ No newline at end of file +services: + # PostgreSQL Database + postgres: + image: postgres:15-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-prostaff_api_development} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "${POSTGRES_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] + interval: 10s + timeout: 5s + retries: 5 + + # Redis for caching and Sidekiq + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + ports: + - "${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # Rails API + api: + build: . + container_name: prostaff-api + env_file: + - .env + volumes: + - .:/app + - bundle_cache:/usr/local/bundle + ports: + - "${API_PORT:-3333}:3000" + depends_on: + redis: + condition: service_healthy + networks: + - default + - security-net + command: > + sh -c " + bundle check || bundle install && + rm -f tmp/pids/server.pid && + bundle exec rails db:migrate && + bundle exec rails server -b 0.0.0.0 -p 3000 + " + + # Sidekiq for background jobs + sidekiq: + build: . + env_file: + - .env + volumes: + - .:/app + - bundle_cache:/usr/local/bundle + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + command: bundle exec sidekiq -C config/sidekiq.yml + +volumes: + postgres_data: + redis_data: + bundle_cache: + +networks: + security-net: + driver: bridge