Initial commit: monorepo Match Live TV.

Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
require_relative "boot"
require "rails"
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
require "active_record/railtie"
require "active_storage/engine"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_mailbox/engine"
require "action_text/engine"
require "action_view/railtie"
require "action_cable/engine"
# require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module App
class Application < Rails::Application
config.load_defaults 7.2
config.autoload_lib(ignore: %w[assets tasks])
config.api_only = false
config.active_job.queue_adapter = :sidekiq
config.time_zone = "Europe/Rome"
config.generators { |g| g.orm :active_record, primary_key_type: :uuid }
end
end

4
backend/config/boot.rb Normal file
View File

@@ -0,0 +1,4 @@
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
require "bundler/setup" # Set up gems listed in the Gemfile.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.

10
backend/config/cable.yml Normal file
View File

@@ -0,0 +1,10 @@
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/0") %>
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL", "redis://localhost:6379/0") %>

View File

@@ -0,0 +1 @@
iUeJHiuhCvxm/D8G4wDoJUfOcttCPg3HCQwWGreLmNBwzZDpxcHwZvNezpVkotHCl9+uwHHXx+u4cncDr2iwD/K7sDjYdd34+wiqXKOIjbuy8yRArndjvcTOpPKoY6JrTCmoJ+NhYz9g4G3NZXxc604MrjuNvOEAr3bYJdH932699WaZJYJbBTODbZ4EFJByOn30qgdSwPHp3aG2R2zMsJn46v00QftdEdAr/Nyy1PergwKtxj0JfI7ChAev3Wy6JBruzONX1krBauSzqkeMK7NCqyboVP8HmJC6SRhvuhuHXGKtCRep1oOdPwyYf0OLMFnyARbTMnxN1W+MDTbs2n6rOg4vpzy3QIgYb/aXLXZd+B8znP44pvN9x963pmaI6Jkimez5UYT0HNV8Df+dhbtHgfE4--pHQJ1krK0oIVvx7F--cqzNinUJSxx+aUQJyyKBdA==

View File

@@ -0,0 +1,83 @@
# PostgreSQL. Versions 9.3 and up are supported.
#
# Install the pg driver:
# gem install pg
# On macOS with Homebrew:
# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
# On Windows:
# gem install pg
# Choose the win32 build.
# Install PostgreSQL and put its /bin directory on your path.
#
# Configure Using Gemfile
# gem "pg"
#
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: app_development
# The specified database role being used to connect to PostgreSQL.
# To create additional roles in PostgreSQL see `$ createuser --help`.
# When left blank, PostgreSQL will use the default role. This is
# the same name as the operating system user running Rails.
#username: app
# The password associated with the PostgreSQL role (username).
#password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
# domain sockets, so uncomment these lines.
#host: localhost
# The TCP port the server listens on. Defaults to 5432.
# If your server runs on a different port number, change accordingly.
#port: 5432
# Schema search path. The server defaults to $user,public
#schema_search_path: myapp,sharedapp,public
# Minimum log levels, in increasing order:
# debug5, debug4, debug3, debug2, debug1,
# log, notice, warning, error, fatal, and panic
# Defaults to warning.
#min_messages: notice
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
database: app_test
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
# ever seen by anyone, they now have access to your database.
#
# Instead, provide the password or a full connection URL as an environment
# variable when you boot the app. For example:
#
# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
#
# If the connection URL is provided in the special DATABASE_URL environment
# variable, Rails will automatically merge its configuration values on top of
# the values provided in this file. Alternatively, you can specify a connection
# URL environment variable explicitly:
#
# production:
# url: <%= ENV["MY_APP_DATABASE_URL"] %>
#
# Read https://guides.rubyonrails.org/configuring.html#configuring-a-database
# for a full overview on how database connection configuration can be specified.
#
production:
<<: *default
url: <%= ENV["DATABASE_URL"] %>

29
backend/config/deploy.yml Normal file
View File

@@ -0,0 +1,29 @@
# Kamal deploy configuration
service: match-live-tv
image: matchlivetv/backend
servers:
web:
hosts:
- YOUR_HETZNER_IP
proxy:
ssl: true
host: api.matchlivetv.example.com
env:
clear:
RAILS_ENV: production
MEDIAMTX_API_URL: http://mediamtx:9997
MEDIAMTX_RTMP_URL: rtmp://mediamtx:1935
secret:
- SECRET_KEY_BASE
- DATABASE_URL
- REDIS_URL
- JWT_SECRET
- MEDIAMTX_WEBHOOK_SECRET
- YOUTUBE_CLIENT_ID
- YOUTUBE_CLIENT_SECRET
builder:
arch: amd64

View File

@@ -0,0 +1,5 @@
# Load the Rails application.
require_relative "application"
# Initialize the Rails application.
Rails.application.initialize!

View File

@@ -0,0 +1,75 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.enable_reloading = true
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable server timing.
config.server_timing = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join("tmp/caching-dev.txt").exist?
config.cache_store = :memory_store
config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" }
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Highlight code that enqueued background job in logs.
config.active_job.verbose_enqueue_logs = true
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
config.action_view.annotate_rendered_view_with_filenames = true
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
# Apply autocorrection by RuboCop to files generated by `bin/rails generate`.
# config.generators.apply_rubocop_autocorrect_after_generate!
end

View File

@@ -0,0 +1,49 @@
require "active_support/core_ext/integer/time"
Rails.application.configure do
config.enable_reloading = false
config.eager_load = true
config.consider_all_requests_local = false
config.active_storage.service = :local
# Nginx Proxy Manager termina TLS
config.assume_ssl = true
config.force_ssl = true
config.ssl_options = {
redirect: {
exclude: ->(request) { request.path == "/up" || request.path == "/cable" }
}
}
config.action_cable.disable_request_forgery_protection = true
if (cable_url = ENV["CABLE_URL"]).present?
config.action_cable.url = cable_url
end
cable_origins = ENV.fetch("CORS_ORIGINS", "*").split(",").map(&:strip)
config.action_cable.allowed_request_origins = cable_origins + [
%r{https?://.*}
]
allowed_hosts = ENV.fetch("ALLOWED_HOSTS", "").split(",").map(&:strip).reject(&:empty?)
config.hosts = allowed_hosts if allowed_hosts.any?
config.host_authorization = {
exclude: ->(request) { request.path == "/up" || request.path.start_with?("/cable") }
}
config.logger = ActiveSupport::Logger.new(STDOUT)
.tap { |logger| logger.formatter = ::Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
config.log_tags = [ :request_id ]
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
config.action_mailer.perform_caching = false
config.i18n.fallbacks = true
config.active_support.report_deprecations = false
config.active_record.dump_schema_after_migration = false
config.active_record.attributes_for_inspect = [ :id ]
end

View File

@@ -0,0 +1,67 @@
require "active_support/core_ext/integer/time"
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# While tests run files are not watched, reloading is not necessary.
config.enable_reloading = false
# Eager loading loads your entire application. When running a single test locally,
# this is usually not necessary, and can slow down your test suite. However, it's
# recommended that you enable it in continuous integration systems to ensure eager
# loading is working properly before deploying your code.
config.eager_load = ENV["CI"].present?
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" }
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.cache_store = :null_store
# Render exception templates for rescuable exceptions and raise for other exceptions.
config.action_dispatch.show_exceptions = :rescuable
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Unlike controllers, the mailer instance doesn't have any context about the
# incoming request so you'll need to provide the :host parameter yourself.
config.action_mailer.default_url_options = { host: "www.example.com" }
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
# Raise error when a before_action's only/except options reference missing actions.
config.action_controller.raise_on_missing_callback_actions = true
end

View File

@@ -0,0 +1 @@
# attr_encrypted uses per-model :key option (see YoutubeCredential, StreamSession)

View File

@@ -0,0 +1,9 @@
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV.fetch("CORS_ORIGINS", "*").split(",")
resource "*",
headers: :any,
methods: %i[get post put patch delete options head],
expose: %w[Authorization]
end
end

View File

@@ -0,0 +1,4 @@
Rails.application.config.filter_parameters += [
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn,
:stream_key, :password, :access_token, :refresh_token, :pairing_token
]

View File

@@ -0,0 +1,16 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, "\\1en"
# inflect.singular /^(ox)en/i, "\\1"
# inflect.irregular "person", "people"
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end

View File

@@ -0,0 +1,55 @@
module MatchLiveTv
class << self
def jwt_secret
ENV.fetch("JWT_SECRET", Rails.application.secret_key_base)
end
def mediamtx_api_url
ENV.fetch("MEDIAMTX_API_URL", "http://localhost:9997")
end
def mediamtx_rtmp_url
ENV.fetch("MEDIAMTX_RTMP_URL", "rtmp://localhost:1935")
end
def mediamtx_webhook_secret
ENV.fetch("MEDIAMTX_WEBHOOK_SECRET", "change_me")
end
def reconnect_timeout_seconds
ENV.fetch("RECONNECT_TIMEOUT_SECONDS", "300").to_i
end
def hls_public_url
ENV.fetch("HLS_PUBLIC_URL", "http://localhost:8888")
end
def mediamtx_hls_url
ENV.fetch("MEDIAMTX_HLS_URL", "http://mediamtx:8888")
end
def app_public_url
ENV.fetch("APP_PUBLIC_URL", "http://localhost:3000")
end
def stripe_secret_key
ENV["STRIPE_SECRET_KEY"].presence
end
def stripe_webhook_secret
ENV["STRIPE_WEBHOOK_SECRET"].presence
end
def stripe_premium_light_price_id
ENV.fetch("STRIPE_PREMIUM_LIGHT_PRICE_ID", ENV.fetch("STRIPE_PREMIUM_PRICE_ID", ""))
end
def stripe_premium_full_price_id
ENV.fetch("STRIPE_PREMIUM_FULL_PRICE_ID", ENV.fetch("STRIPE_PREMIUM_PRICE_ID", ""))
end
def stripe_enabled?
stripe_secret_key.present?
end
end
end

View File

@@ -0,0 +1,11 @@
class Rack::Attack
throttle("api/ip", limit: 300, period: 5.minutes) do |req|
req.ip if req.path.start_with?("/api/")
end
throttle("auth/ip", limit: 20, period: 5.minutes) do |req|
req.ip if req.path.include?("/auth/login")
end
end
Rails.application.config.middleware.use Rack::Attack if defined?(Rack::Attack)

View File

@@ -0,0 +1,9 @@
require "sidekiq/api"
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
end

View File

@@ -0,0 +1,2 @@
# Schedule CleanupExpiredSessionsJob hourly via host cron or Kamal:
# 0 * * * * cd /app && bundle exec rails runner "CleanupExpiredSessionsJob.perform_async"

View File

@@ -0,0 +1,3 @@
if MatchLiveTv.stripe_secret_key.present?
Stripe.api_key = MatchLiveTv.stripe_secret_key
end

View File

@@ -0,0 +1,31 @@
# Files in the config/locales directory are used for internationalization and
# are automatically loaded by Rails. If you want to use locales other than
# English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t "hello"
#
# In views, this is aliased to just `t`:
#
# <%= t("hello") %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more about the API, please read the Rails Internationalization guide
# at https://guides.rubyonrails.org/i18n.html.
#
# Be aware that YAML interprets the following case-insensitive strings as
# booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings
# must be quoted to be interpreted as strings. For example:
#
# en:
# "yes": yup
# enabled: "ON"
en:
hello: "Hello world"

34
backend/config/puma.rb Normal file
View File

@@ -0,0 +1,34 @@
# This configuration file will be evaluated by Puma. The top-level methods that
# are invoked here are part of Puma's configuration DSL. For more information
# about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html.
# Puma starts a configurable number of processes (workers) and each process
# serves each request in a thread from an internal thread pool.
#
# The ideal number of threads per worker depends both on how much time the
# application spends waiting for IO operations and on how much you wish to
# to prioritize throughput over latency.
#
# As a rule of thumb, increasing the number of threads will increase how much
# traffic a given process can handle (throughput), but due to CRuby's
# Global VM Lock (GVL) it has diminishing returns and will degrade the
# response time (latency) of the application.
#
# The default is set to 3 threads as it's deemed a decent compromise between
# throughput and latency for the average Rails application.
#
# Any libraries that use a connection pool or another resource pool should
# be configured to provide at least as many connections as the number of
# threads. This includes Active Record's `pool` parameter in `database.yml`.
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
port ENV.fetch("PORT", 3000)
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
# Specify the PID file. Defaults to tmp/pids/server.pid in development.
# In other environments, only set the PID file if requested.
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

96
backend/config/routes.rb Normal file
View File

@@ -0,0 +1,96 @@
Rails.application.routes.draw do
mount ActionCable.server => "/cable"
get "up" => "rails/health#show", as: :rails_health_check
namespace :api do
namespace :v1 do
post "auth/login", to: "auth#login"
post "auth/register", to: "auth#register"
post "auth/logout", to: "auth#logout"
post "auth/refresh", to: "auth#refresh"
get "auth/me", to: "auth#me"
resources :teams do
member do
post :members, action: :add_member
delete "members/:user_id", action: :remove_member
get :recordings
end
resources :matches, only: %i[index create]
get "youtube/authorize", to: "youtube#authorize"
end
resources :matches, only: %i[show update destroy] do
resources :sessions, only: %i[create], controller: "stream_sessions"
end
resources :sessions, only: %i[show], controller: "stream_sessions" do
member do
patch :start
patch :stop
patch :pause
get :events
post :telemetry
post :pairing_token
post :claim_pairing
post :network_test
get :youtube_stats
end
end
get "youtube/callback", to: "youtube#callback"
end
end
namespace :webhooks do
post "mediamtx/connect", to: "mediamtx#connect"
post "mediamtx/disconnect", to: "mediamtx#disconnect"
post "mediamtx/ready", to: "mediamtx#ready"
post "mediamtx/validate_publish", to: "mediamtx#validate_publish"
post "stripe", to: "stripe#create"
end
post "internal/validate_publish", to: "webhooks/mediamtx#validate_publish"
namespace :admin do
root to: "dashboard#index"
resources :teams, only: %i[index show]
resources :sessions, only: %i[index show]
end
get "hls/*path", to: "hls_proxy#show", format: false, constraints: { path: /.+/ }
get "live", to: "public/live#index", as: :public_live_index
get "live/:id", to: "public/live#show", as: :public_live
get "live/:id/status.json", to: "public/live#status", as: :public_live_status
get "replay/:id", to: "public/replay#show", as: :public_replay
root to: "public/pages#home"
scope module: :public, as: :public do
get "funzionalita", to: "pages#features", as: :features
get "prezzi", to: "pages#pricing", as: :prezzi
get "pricing", to: redirect("/prezzi")
get "privacy", to: "pages#privacy", as: :privacy
get "termini", to: "pages#terms", as: :termini
get "signup", to: "registrations#new"
post "signup", to: "registrations#create"
get "login", to: "sessions#new"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "teams/new", to: "teams#new"
post "teams", to: "teams#create"
get "teams/:id/dashboard", to: "teams#dashboard", as: :team_dashboard
get "teams/:id/billing", to: "teams#billing", as: :team_billing
get "teams/:id/checkout", to: "teams#checkout", as: :team_checkout
post "teams/:id/portal", to: "teams#portal", as: :team_portal
get "teams/:id/invite", to: "teams#invite", as: :team_invite
post "teams/:id/invite", to: "teams#create_invitation"
delete "teams/:id/members/:user_id", to: "teams#remove_member", as: :team_remove_member
delete "teams/:id/invitations/:invitation_id", to: "teams#destroy_invitation", as: :team_destroy_invitation
get "join/:token", to: "invitations#show", as: :invitation
post "join/:token", to: "invitations#accept"
end
end

View File

@@ -0,0 +1,4 @@
:concurrency: 5
:queues:
- default
- critical

View File

@@ -0,0 +1,34 @@
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket-<%= Rails.env %>
# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket-<%= Rails.env %>
# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name-<%= Rails.env %>
# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]