Club come entità principale, branding e fix infrastruttura streaming.

Introduce clubs con abbonamento, branding Active Storage e flusso registrazione società; corregge healthcheck MediaMTX (immagine distroless) e allinea dominio produzione matchlivetv.it.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-27 09:16:24 +02:00
parent 471291b2c4
commit 4083bc5dee
53 changed files with 1120 additions and 197 deletions

View File

@@ -0,0 +1,108 @@
module Public
class ClubsController < WebBaseController
include BrandingAttachments
before_action :require_login!
before_action :set_club, only: %i[show edit update billing checkout portal]
def new
redirect_to public_club_path(current_user.primary_club) if current_user.primary_club
end
def create
if current_user.owned_clubs.exists?
redirect_to public_club_path(current_user.primary_club), alert: "Hai già registrato una società"
return
end
club = Club.new(club_params)
attach_branding_logo(club)
club.save!
ClubMembership.create!(user: current_user, club: club, role: "owner")
first_team_name = params.dig(:first_team, :name).presence || "Prima squadra"
club.teams.create!(
name: first_team_name,
sport: club.sport
)
plan = params[:plan].presence_in(%w[free premium_light premium_full]) || "free"
Billing::AssignPlan.call(club: club, plan_slug: plan)
if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled?
redirect_to public_club_checkout_path(club, plan: plan)
else
redirect_to public_club_path(club), notice: "Società e prima squadra create."
end
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity
end
def show
require_club_owner!(@club)
@entitlements_team = @club.teams.first
@entitlements = @entitlements_team&.entitlements
@teams = @club.teams.order(:name)
end
def edit
require_club_owner!(@club)
end
def update
require_club_owner!(@club)
@club.assign_attributes(club_params)
attach_branding_logo(@club)
@club.save!
redirect_to public_club_path(@club), notice: "Società aggiornata."
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
end
def billing
require_club_owner!(@club)
@team = @club.teams.first!
@entitlements = @team.entitlements
@plans = Plan.ordered
end
def checkout
require_club_owner!(@club)
unless MatchLiveTv.stripe_enabled?
redirect_to public_club_billing_path(@club), alert: "Pagamenti non ancora configurati sul server"
return
end
plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) || "premium_light"
url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user, plan_slug: plan_slug).url
redirect_to url, allow_other_host: true
end
def portal
require_club_owner!(@club)
unless MatchLiveTv.stripe_enabled?
redirect_to public_club_billing_path(@club), alert: "Portale pagamenti non configurato"
return
end
url = Billing::Stripe::CheckoutSession.new(club: @club, user: current_user).portal_url
redirect_to url, allow_other_host: true
end
private
def set_club
@club = current_user.owned_clubs.find(params[:id])
end
def club_params
p = params.require(:club).permit(:name, :sport, :logo_url, :primary_color, :secondary_color)
p[:primary_color] = normalize_hex_color(p[:primary_color], "#e53935")
p[:secondary_color] = normalize_hex_color(p[:secondary_color], "#ffffff")
p
end
end
end

View File

@@ -1,7 +1,7 @@
module Public
class RegistrationsController < WebBaseController
def new
redirect_to public_team_dashboard_path(current_user.teams.first) if logged_in? && current_user.teams.any?
redirect_to public_club_path(current_user.primary_club) if logged_in? && current_user.primary_club
@user = User.new
end
@@ -23,7 +23,7 @@ module Public
return redirect_to public_team_dashboard_path(invitation.team), notice: "Benvenuto nella squadra!"
end
end
redirect_to new_public_team_path, notice: "Account creato. Ora crea la tua squadra."
redirect_to public_new_club_path, notice: "Account creato. Ora registra la tua società."
else
flash.now[:alert] = @user.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity

View File

@@ -1,14 +1,22 @@
module Public
class SessionsController < WebBaseController
def new
redirect_to public_team_dashboard_path(current_user.teams.first) if logged_in? && current_user.teams.any?
if logged_in? && current_user.primary_club
redirect_to public_club_path(current_user.primary_club)
end
end
def create
user = User.find_by(email: params[:email]&.downcase)
if user&.authenticate(params[:password])
session[:user_id] = user.id
dest = user.teams.first ? public_team_dashboard_path(user.teams.first) : new_public_team_path
dest = if user.primary_club
public_club_path(user.primary_club)
elsif user.manageable_teams.first
public_team_dashboard_path(user.manageable_teams.first)
else
public_new_club_path
end
redirect_to dest, notice: "Bentornato!"
else
flash.now[:alert] = "Email o password non validi"

View File

@@ -1,82 +1,89 @@
module Public
class TeamsController < WebBaseController
include BrandingAttachments
before_action :require_login!
before_action :set_club, only: %i[new create]
before_action :set_team, only: %i[
dashboard billing checkout portal invite create_invitation
remove_member destroy_invitation
dashboard edit update invite create_invitation
assign_self_staff clear_self_staff remove_member destroy_invitation
]
def new
redirect_to public_team_dashboard_path(current_user.teams.first) if current_user.teams.any?
redirect_to public_new_club_path if current_user.owned_clubs.none?
require_club_owner!(@club)
end
def create
if current_user.user_teams.where(role: "owner").exists?
redirect_to public_team_dashboard_path(current_user.teams.first), alert: "Hai già una squadra"
return
end
team = Team.create!(team_params)
UserTeam.create!(user: current_user, team: team, role: "owner")
plan = params[:plan].presence_in(%w[free premium_light premium_full]) || "free"
Billing::AssignPlan.call(team: team, plan_slug: plan)
if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled?
redirect_to public_team_checkout_path(team, plan: plan)
else
redirect_to public_team_dashboard_path(team), notice: "Squadra creata con piano Free"
end
require_club_owner!(@club)
team = @club.teams.create!(team_params)
attach_branding_logo(team)
redirect_to public_club_path(@club), notice: "Squadra «#{team.name}» aggiunta."
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity
end
def dashboard
require_team_owner!(@team)
require_club_owner_for_team!(@team)
@club = @team.club
@entitlements = @team.entitlements
@recordings = @team.recordings.ready.includes(stream_session: :match).order(created_at: :desc).limit(10)
@members = @team.user_teams.includes(:user).where.not(role: "owner")
@owner_membership = current_user.user_teams.find_by(team: @team)
@staff_memberships = @team.user_teams.includes(:user)
.where("user_teams.role = 'member' OR (user_teams.staff_kind IS NOT NULL)")
@pending_invitations = @team.team_invitations.pending.order(created_at: :desc)
end
def billing
require_team_owner!(@team)
@entitlements = @team.entitlements
@plans = Plan.ordered
def edit
require_club_owner_for_team!(@team)
@club = @team.club
end
def checkout
require_team_owner!(@team)
unless MatchLiveTv.stripe_enabled?
redirect_to public_team_billing_path(@team), alert: "Pagamenti non ancora configurati sul server"
return
end
plan_slug = params[:plan].presence_in(%w[premium_light premium_full]) || "premium_light"
url = Billing::Stripe::CheckoutSession.new(team: @team, user: current_user, plan_slug: plan_slug).url
redirect_to url, allow_other_host: true
end
def portal
require_team_owner!(@team)
unless MatchLiveTv.stripe_enabled?
redirect_to public_team_billing_path(@team), alert: "Portale pagamenti non configurato"
return
end
url = Billing::Stripe::CheckoutSession.new(team: @team, user: current_user).portal_url
redirect_to url, allow_other_host: true
def update
require_club_owner_for_team!(@team)
@team.assign_attributes(team_params)
attach_branding_logo(@team)
@team.save!
redirect_to public_team_dashboard_path(@team), notice: "Squadra aggiornata."
rescue ActiveRecord::RecordInvalid => e
@club = @team.club
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
end
def invite
require_team_owner!(@team)
require_club_owner_for_team!(@team)
@entitlements = @team.entitlements
end
def assign_self_staff
require_club_owner_for_team!(@team)
staff_kind = params[:staff_kind].presence_in(TeamInvitation::STAFF_KINDS) || "transmission"
membership = current_user.user_teams.find_or_initialize_by(team: @team)
membership.role = "member" if membership.new_record?
Teams::StaffAssignment.call(team: @team, user: current_user, staff_kind: staff_kind, membership: membership)
label = staff_kind == "regia" ? "Regia" : "Trasmissione"
redirect_to public_team_dashboard_path(@team), notice: "Il tuo account è ora staff #{label}."
rescue Teams::StaffAssignmentError, Teams::EntitlementError => e
redirect_to public_team_dashboard_path(@team), alert: e.message
end
def clear_self_staff
require_club_owner_for_team!(@team)
membership = current_user.user_teams.find_by(team: @team)
membership&.update!(staff_kind: nil)
redirect_to public_team_dashboard_path(@team), notice: "Ruolo staff rimosso dal tuo account."
end
def create_invitation
require_team_owner!(@team)
require_club_owner_for_team!(@team)
@entitlements = @team.entitlements
staff_kind = params[:staff_kind].presence_in(TeamInvitation::STAFF_KINDS) || "transmission"
@entitlements.assert_can_invite!(staff_kind: staff_kind)
email = params[:email]&.downcase&.strip
Teams::StaffEmailValidator.assert_available!(team: @team, email: email, staff_kind: staff_kind)
token = TeamInvitation.generate_token
@team.team_invitations.create!(
email: email,
@@ -88,19 +95,19 @@ module Public
@invite_url = join_public_invitation_url(token: token)
flash.now[:notice] = "Link invito generato (valido 7 giorni)"
render :invite
rescue Teams::EntitlementError => e
rescue Teams::EntitlementError, Teams::StaffAssignmentError => e
redirect_to public_team_invite_path(@team), alert: e.message
end
def remove_member
require_team_owner!(@team)
require_club_owner_for_team!(@team)
ut = @team.user_teams.find_by!(user_id: params[:user_id], role: "member")
ut.destroy!
redirect_to public_team_dashboard_path(@team), notice: "Accesso revocato"
end
def destroy_invitation
require_team_owner!(@team)
require_club_owner_for_team!(@team)
inv = @team.team_invitations.pending.find(params[:invitation_id])
inv.destroy!
redirect_to public_team_dashboard_path(@team), notice: "Invito annullato"
@@ -108,12 +115,25 @@ module Public
private
def set_club
@club = current_user.owned_clubs.find(params[:club_id])
end
def set_team
@team = current_user.teams.find(params[:id])
@team = current_user.manageable_teams.find(params[:id])
end
def team_params
params.require(:team).permit(:name, :sport, :logo_url)
p = params.require(:team).permit(:name, :sport, :logo_url, :primary_color, :secondary_color)
p[:primary_color] = p[:primary_color].presence
p[:secondary_color] = p[:secondary_color].presence
if p[:primary_color].present?
p[:primary_color] = normalize_hex_color(p[:primary_color], p[:primary_color])
end
if p[:secondary_color].present?
p[:secondary_color] = normalize_hex_color(p[:secondary_color], p[:secondary_color])
end
p
end
end
end

View File

@@ -10,9 +10,12 @@ module Public
redirect_to public_login_path, alert: "Accedi per continuare"
end
def require_team_owner!(team)
membership = current_user.user_teams.find_by(team: team)
redirect_to public_prezzi_path, alert: "Accesso non consentito" unless membership&.role == "owner"
def require_club_owner!(club)
redirect_to public_prezzi_path, alert: "Accesso non consentito" unless club.owned_by?(current_user)
end
def require_club_owner_for_team!(team)
redirect_to public_prezzi_path, alert: "Accesso non consentito" unless team.club&.owned_by?(current_user)
end
end
end