Merge branch 'feature/password-policy'
Policy password, errori API localizzati, fix UI mobile marketing e handoff iOS.
This commit is contained in:
@@ -9,13 +9,23 @@ module Admin
|
|||||||
return render :edit, status: :unprocessable_entity
|
return render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:password].blank? || params[:password].length < 8
|
if params[:password] != params[:password_confirmation]
|
||||||
flash.now[:alert] = t("admin.flash.password_too_short")
|
flash.now[:alert] = t("admin.flash.password_mismatch")
|
||||||
return render :edit, status: :unprocessable_entity
|
return render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:password] != params[:password_confirmation]
|
if (code = PasswordComplexity.violation(params[:password]))
|
||||||
flash.now[:alert] = t("admin.flash.password_mismatch")
|
key = case code
|
||||||
|
when :blank, :too_short then :password_too_short
|
||||||
|
when :too_long then :password_too_long
|
||||||
|
else :password_too_weak
|
||||||
|
end
|
||||||
|
flash.now[:alert] = t("admin.flash.#{key}")
|
||||||
|
return render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
if PasswordComplexity.same_as_current?(current_admin_account, params[:password])
|
||||||
|
flash.now[:alert] = t("admin.flash.password_same_as_current")
|
||||||
return render :edit, status: :unprocessable_entity
|
return render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module Api
|
|||||||
def update
|
def update
|
||||||
name = params[:name].to_s.strip
|
name = params[:name].to_s.strip
|
||||||
if name.blank?
|
if name.blank?
|
||||||
return render json: { error: "Name is required" }, status: :unprocessable_entity
|
return render json: { error: I18n.t("flash.accounts.name_required") }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if current_user.update(name: name)
|
if current_user.update(name: name)
|
||||||
@@ -30,7 +30,7 @@ module Api
|
|||||||
return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity
|
return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: { message: "Password updated" }
|
render json: { message: I18n.t("flash.accounts.password_updated") }
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@@ -46,10 +46,13 @@ module Api
|
|||||||
|
|
||||||
def password_error_message(code)
|
def password_error_message(code)
|
||||||
case code
|
case code
|
||||||
when :current_incorrect then "Current password is incorrect"
|
when :current_incorrect then I18n.t("flash.accounts.password_current_incorrect")
|
||||||
when :too_short then "Password must be at least 8 characters"
|
when :too_short then I18n.t("flash.accounts.password_too_short")
|
||||||
when :mismatch then "Passwords do not match"
|
when :too_long then I18n.t("flash.accounts.password_too_long")
|
||||||
else "Unable to update password"
|
when :too_weak then I18n.t("flash.accounts.password_too_weak")
|
||||||
|
when :same_as_current then I18n.t("flash.accounts.password_same_as_current")
|
||||||
|
when :mismatch then I18n.t("flash.accounts.password_mismatch")
|
||||||
|
else I18n.t("flash.accounts.password_update_failed")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,18 +22,18 @@ module Api
|
|||||||
if user&.authenticate(params[:password])
|
if user&.authenticate(params[:password])
|
||||||
render json: token_response(user), status: :ok
|
render json: token_response(user), status: :ok
|
||||||
else
|
else
|
||||||
render json: { error: "Invalid credentials" }, status: :unauthorized
|
render json: { error: I18n.t("flash.sessions.invalid_credentials") }, status: :unauthorized
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def logout
|
def logout
|
||||||
render json: { message: "Logged out" }
|
render json: { message: I18n.t("flash.sessions.logged_out") }
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh
|
def refresh
|
||||||
payload = JsonWebToken.decode(params[:refresh_token] || bearer_token)
|
payload = JsonWebToken.decode(params[:refresh_token] || bearer_token)
|
||||||
user = User.find_by(id: payload&.dig(:user_id))
|
user = User.find_by(id: payload&.dig(:user_id))
|
||||||
return render json: { error: "Invalid token" }, status: :unauthorized unless user
|
return render json: { error: I18n.t("flash.sessions.invalid_token") }, status: :unauthorized unless user
|
||||||
|
|
||||||
render json: token_response(user)
|
render json: token_response(user)
|
||||||
end
|
end
|
||||||
@@ -45,7 +45,7 @@ module Api
|
|||||||
def forgot_password
|
def forgot_password
|
||||||
Users::RequestPasswordReset.call(email: params[:email])
|
Users::RequestPasswordReset.call(email: params[:email])
|
||||||
render json: {
|
render json: {
|
||||||
message: "If the email is registered, you will receive a password reset link shortly."
|
message: I18n.t("flash.password_resets.email_sent")
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
class ApplicationController < ActionController::API
|
class ApplicationController < ActionController::API
|
||||||
include ActionController::HttpAuthentication::Token::ControllerMethods
|
include ActionController::HttpAuthentication::Token::ControllerMethods
|
||||||
|
|
||||||
|
before_action :set_api_locale
|
||||||
before_action :authenticate_request!
|
before_action :authenticate_request!
|
||||||
|
|
||||||
attr_reader :current_user
|
attr_reader :current_user
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def set_api_locale
|
||||||
|
explicit = LocaleResolver.normalize(request.headers["X-Locale"])
|
||||||
|
I18n.locale = explicit ||
|
||||||
|
LocaleResolver.from_accept_language(request.headers["Accept-Language"]) ||
|
||||||
|
I18n.default_locale
|
||||||
|
end
|
||||||
|
|
||||||
def authenticate_request!
|
def authenticate_request!
|
||||||
token = bearer_token
|
token = bearer_token
|
||||||
payload = JsonWebToken.decode(token)
|
payload = JsonWebToken.decode(token)
|
||||||
@current_user = User.find_by(id: payload[:user_id]) if payload
|
@current_user = User.find_by(id: payload[:user_id]) if payload
|
||||||
render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
|
render json: { error: I18n.t("flash.sessions.unauthorized") }, status: :unauthorized unless @current_user
|
||||||
end
|
end
|
||||||
|
|
||||||
def bearer_token
|
def bearer_token
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ module Public
|
|||||||
)
|
)
|
||||||
|
|
||||||
unless result.ok?
|
unless result.ok?
|
||||||
flash.now[:alert] = t("flash.accounts.password_#{result.error}")
|
redirect_to public_account_path, alert: t("flash.accounts.password_#{result.error}")
|
||||||
return render :show, status: :unprocessable_entity
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to public_account_path, notice: t("flash.accounts.password_updated")
|
redirect_to public_account_path, notice: t("flash.accounts.password_updated")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module Public
|
|||||||
def edit
|
def edit
|
||||||
@user = User.find_by_password_reset_token(params[:token])
|
@user = User.find_by_password_reset_token(params[:token])
|
||||||
if @user.nil? || @user.password_reset_expired?
|
if @user.nil? || @user.password_reset_expired?
|
||||||
redirect_to new_public_password_reset_path,
|
redirect_to public_password_forgot_path,
|
||||||
alert: t("flash.password_resets.invalid_or_expired_link")
|
alert: t("flash.password_resets.invalid_or_expired_link")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -23,19 +23,25 @@ module Public
|
|||||||
def update
|
def update
|
||||||
@user = User.find_by_password_reset_token(params[:token])
|
@user = User.find_by_password_reset_token(params[:token])
|
||||||
if @user.nil? || @user.password_reset_expired?
|
if @user.nil? || @user.password_reset_expired?
|
||||||
redirect_to new_public_password_reset_path,
|
redirect_to public_password_forgot_path,
|
||||||
alert: t("flash.password_resets.invalid_or_expired_link")
|
alert: t("flash.password_resets.invalid_or_expired_link")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:password].blank? || params[:password].length < 8
|
if params[:password] != params[:password_confirmation]
|
||||||
flash.now[:alert] = t("flash.password_resets.password_min_length")
|
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
||||||
@token = params[:token]
|
@token = params[:token]
|
||||||
return render :edit, status: :unprocessable_entity
|
return render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:password] != params[:password_confirmation]
|
if (code = PasswordComplexity.violation(params[:password]))
|
||||||
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
flash.now[:alert] = t("flash.password_resets.password_#{code == :blank ? :too_short : code}")
|
||||||
|
@token = params[:token]
|
||||||
|
return render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
if PasswordComplexity.same_as_current?(@user, params[:password])
|
||||||
|
flash.now[:alert] = t("flash.password_resets.password_same_as_current")
|
||||||
@token = params[:token]
|
@token = params[:token]
|
||||||
return render :edit, status: :unprocessable_entity
|
return render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class AdminAccount < ApplicationRecord
|
class AdminAccount < ApplicationRecord
|
||||||
|
include PasswordComplexity
|
||||||
|
|
||||||
has_secure_password
|
has_secure_password
|
||||||
|
|
||||||
validates :username, presence: true, uniqueness: true
|
validates :username, presence: true, uniqueness: true
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Criteri "di mercato" (stile Cognito/Auth0 bilanciato):
|
||||||
|
# - minimo 8 caratteri (max 72 per bcrypt)
|
||||||
|
# - almeno 3 classi su 4: minuscole, maiuscole, numeri, simboli
|
||||||
|
module PasswordComplexity
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
MIN_LENGTH = 8
|
||||||
|
MAX_LENGTH = 72
|
||||||
|
REQUIRED_CLASSES = 3
|
||||||
|
|
||||||
|
CLASS_CHECKS = {
|
||||||
|
lowercase: /[a-z]/,
|
||||||
|
uppercase: /[A-Z]/,
|
||||||
|
digit: /\d/,
|
||||||
|
symbol: /[^A-Za-z0-9]/
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
class << self
|
||||||
|
def violation(password)
|
||||||
|
value = password.to_s
|
||||||
|
return :blank if value.blank?
|
||||||
|
return :too_short if value.length < MIN_LENGTH
|
||||||
|
return :too_long if value.bytesize > MAX_LENGTH
|
||||||
|
return :too_weak unless strong_enough?(value)
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def strong_enough?(password)
|
||||||
|
matched = CLASS_CHECKS.count { |_, pattern| password.match?(pattern) }
|
||||||
|
matched >= REQUIRED_CLASSES
|
||||||
|
end
|
||||||
|
|
||||||
|
# Confronta con il digest già salvato (prima di assegnare la nuova password).
|
||||||
|
def same_as_current?(record, password)
|
||||||
|
return false if password.blank? || !record.respond_to?(:authenticate)
|
||||||
|
|
||||||
|
record.authenticate(password).present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def requirement_summary
|
||||||
|
I18n.t("password_policy.hint")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
included do
|
||||||
|
validate :password_meets_complexity_policy, if: -> { password.present? }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def password_meets_complexity_policy
|
||||||
|
case PasswordComplexity.violation(password)
|
||||||
|
when :too_short
|
||||||
|
errors.add(:password, :too_short, count: MIN_LENGTH)
|
||||||
|
when :too_long
|
||||||
|
errors.add(:password, :too_long, count: MAX_LENGTH)
|
||||||
|
when :too_weak
|
||||||
|
errors.add(:password, :complexity)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
include PasswordComplexity
|
||||||
|
|
||||||
ROLES = %w[admin coach parent volunteer].freeze
|
ROLES = %w[admin coach parent volunteer].freeze
|
||||||
|
|
||||||
has_secure_password
|
has_secure_password
|
||||||
|
|||||||
@@ -23,15 +23,23 @@ module Users
|
|||||||
return Result.new(ok?: false, error: :current_incorrect)
|
return Result.new(ok?: false, error: :current_incorrect)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @password.blank? || @password.length < 8
|
|
||||||
return Result.new(ok?: false, error: :too_short)
|
|
||||||
end
|
|
||||||
|
|
||||||
if @password != @password_confirmation
|
if @password != @password_confirmation
|
||||||
return Result.new(ok?: false, error: :mismatch)
|
return Result.new(ok?: false, error: :mismatch)
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.update!(password: @password)
|
if (code = PasswordComplexity.violation(@password))
|
||||||
|
return Result.new(ok?: false, error: code == :blank ? :too_short : code)
|
||||||
|
end
|
||||||
|
|
||||||
|
if PasswordComplexity.same_as_current?(@user, @password)
|
||||||
|
return Result.new(ok?: false, error: :same_as_current)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless @user.update(password: @password)
|
||||||
|
complexity_error = @user.errors.details[:password]&.any? { |d| d[:error] == :complexity }
|
||||||
|
return Result.new(ok?: false, error: complexity_error ? :too_weak : :too_short)
|
||||||
|
end
|
||||||
|
|
||||||
@user.clear_password_reset!
|
@user.clear_password_reset!
|
||||||
Result.new(ok?: true)
|
Result.new(ok?: true)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<%= render "shared/meta_tags" %>
|
<%= render "shared/meta_tags" %>
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="/marketing.css?v=44">
|
<link rel="stylesheet" href="/marketing.css?v=50">
|
||||||
</head>
|
</head>
|
||||||
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
|
||||||
<%= render "shared/cookie_banner" %>
|
<%= render "shared/cookie_banner" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
|
||||||
<%= render "shared/meta_tags" %>
|
<%= render "shared/meta_tags" %>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
<link rel="stylesheet" href="/marketing.css?v=44">
|
<link rel="stylesheet" href="/marketing.css?v=50">
|
||||||
<link rel="stylesheet" href="/live.css?v=26">
|
<link rel="stylesheet" href="/live.css?v=26">
|
||||||
<%= yield :head %>
|
<%= yield :head %>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2>
|
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2>
|
||||||
|
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
|
||||||
<%= form_with url: public_account_password_path, method: :patch, local: true do %>
|
<%= form_with url: public_account_password_path, method: :patch, local: true do %>
|
||||||
<%= render "shared/input_toggle",
|
<%= render "shared/input_toggle",
|
||||||
name: :current_password,
|
name: :current_password,
|
||||||
|
|||||||
@@ -2,25 +2,27 @@
|
|||||||
<% content_for :meta_description, t("auth.invitation.meta_description") %>
|
<% content_for :meta_description, t("auth.invitation.meta_description") %>
|
||||||
<% content_for :robots, "noindex, nofollow" %>
|
<% content_for :robots, "noindex, nofollow" %>
|
||||||
|
|
||||||
<div class="card" style="max-width:480px">
|
<section class="auth-page">
|
||||||
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
|
<div class="card">
|
||||||
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
|
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
|
||||||
<p class="muted" style="font-size:0.9rem;margin-bottom:16px">
|
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
|
||||||
<%= raw t("auth.invitation.instructions_html", email: @invitation.email) %>
|
<p class="muted" style="font-size:0.9rem;margin-bottom:16px">
|
||||||
</p>
|
<%= raw t("auth.invitation.instructions_html", email: @invitation.email) %>
|
||||||
<% if logged_in? %>
|
</p>
|
||||||
<%= button_to t("auth.invitation.accept"), public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
|
<% if logged_in? %>
|
||||||
<% else %>
|
<%= button_to t("auth.invitation.accept"), public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
|
||||||
<p><%= raw t(
|
<% else %>
|
||||||
"auth.invitation.login_or_signup_html",
|
<p><%= raw t(
|
||||||
login_link: link_to(t("auth.invitation.login_link"), public_login_path),
|
"auth.invitation.login_or_signup_html",
|
||||||
signup_link: link_to(t("auth.invitation.signup_link"), public_signup_path),
|
login_link: link_to(t("auth.invitation.login_link"), public_login_path),
|
||||||
email: @invitation.email
|
signup_link: link_to(t("auth.invitation.signup_link"), public_signup_path),
|
||||||
) %></p>
|
email: @invitation.email
|
||||||
<%= button_to t("auth.invitation.accept_if_logged_in"), public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
|
) %></p>
|
||||||
<% end %>
|
<%= button_to t("auth.invitation.accept_if_logged_in"), public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
|
||||||
<p style="margin-top:16px;font-size:0.88rem;color:#888">
|
<% end %>
|
||||||
<%= t("auth.invitation.mobile_app_label") %>
|
<p style="margin-top:16px;font-size:0.88rem;color:#888">
|
||||||
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
|
<%= t("auth.invitation.mobile_app_label") %>
|
||||||
</p>
|
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|||||||
@@ -46,7 +46,8 @@
|
|||||||
|
|
||||||
<h3><%= t("legal.cookies.s4_1_title") %></h3>
|
<h3><%= t("legal.cookies.s4_1_title") %></h3>
|
||||||
<p><%= t("legal.cookies.s4_1_intro") %></p>
|
<p><%= t("legal.cookies.s4_1_intro") %></p>
|
||||||
<table class="legal-table">
|
<div class="table-scroll">
|
||||||
|
<table class="legal-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
<tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -64,7 +65,8 @@
|
|||||||
<td><%= t("legal.cookies.s4_1_row2_provider") %></td>
|
<td><%= t("legal.cookies.s4_1_row2_provider") %></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3><%= t("legal.cookies.s4_2_title") %></h3>
|
<h3><%= t("legal.cookies.s4_2_title") %></h3>
|
||||||
<p>
|
<p>
|
||||||
@@ -75,7 +77,8 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p>
|
<p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<table class="legal-table">
|
<div class="table-scroll">
|
||||||
|
<table class="legal-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
<tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -99,7 +102,8 @@
|
|||||||
<td><%= t("legal.cookies.s4_2_row3_provider") %></td>
|
<td><%= t("legal.cookies.s4_2_row3_provider") %></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<%= raw t(
|
<%= raw t(
|
||||||
"legal.cookies.s4_2_p2_html",
|
"legal.cookies.s4_2_p2_html",
|
||||||
|
|||||||
@@ -22,20 +22,22 @@
|
|||||||
|
|
||||||
<%= render "shared/plan_cards" %>
|
<%= render "shared/plan_cards" %>
|
||||||
|
|
||||||
<table class="compare-table">
|
<div class="table-scroll">
|
||||||
<thead>
|
<table class="compare-table">
|
||||||
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
|
<thead>
|
||||||
</thead>
|
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
|
||||||
<tbody>
|
</thead>
|
||||||
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr>
|
<tbody>
|
||||||
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
|
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
|
||||||
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr>
|
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
|
||||||
</tbody>
|
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr>
|
||||||
</table>
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-top:32px;text-align:center">
|
<div class="card" style="margin-top:32px;text-align:center">
|
||||||
<h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
|
<h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
|
||||||
|
|||||||
@@ -77,7 +77,8 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2><%= t("legal.privacy.s5_title") %></h2>
|
<h2><%= t("legal.privacy.s5_title") %></h2>
|
||||||
<table class="legal-table">
|
<div class="table-scroll">
|
||||||
|
<table class="legal-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr>
|
<tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -107,7 +108,8 @@
|
|||||||
<td><%= t("legal.privacy.s5_row6_basis") %></td>
|
<td><%= t("legal.privacy.s5_row6_basis") %></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<section class="auth-page">
|
<section class="auth-page">
|
||||||
<h1><%= t("auth.password_reset.title") %></h1>
|
<h1><%= t("auth.password_reset.title") %></h1>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<p class="muted" style="font-size:0.9rem"><%= t("password_policy.hint") %></p>
|
||||||
<%= form_with url: public_password_reset_path, method: :patch, local: true do %>
|
<%= form_with url: public_password_reset_path, method: :patch, local: true do %>
|
||||||
<%= hidden_field_tag :token, @token %>
|
<%= hidden_field_tag :token, @token %>
|
||||||
<%= render "shared/input_toggle",
|
<%= render "shared/input_toggle",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
required: true,
|
required: true,
|
||||||
minlength: 8,
|
minlength: 8,
|
||||||
autocomplete: "new-password" %>
|
autocomplete: "new-password" %>
|
||||||
|
<p class="muted" style="font-size:0.9rem;margin-top:-0.5rem"><%= t("password_policy.hint") %></p>
|
||||||
<%= render "shared/input_toggle",
|
<%= render "shared/input_toggle",
|
||||||
name: "user[password_confirmation]",
|
name: "user[password_confirmation]",
|
||||||
id: "user_password_confirmation",
|
id: "user_password_confirmation",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
<h3 style="margin-top:28px;font-size:1.1rem"><%= t("billing.documents.table_heading") %></h3>
|
<h3 style="margin-top:28px;font-size:1.1rem"><%= t("billing.documents.table_heading") %></h3>
|
||||||
<% if payments.any? %>
|
<% if payments.any? %>
|
||||||
|
<div class="table-scroll">
|
||||||
<table class="data billing-table">
|
<table class="data billing-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<p style="color:#888"><%= t("billing.documents.no_payments") %></p>
|
<p style="color:#888"><%= t("billing.documents.no_payments") %></p>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -58,10 +58,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle.addEventListener("click", function (e) {
|
toggle.addEventListener("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setOpen(menu.hidden);
|
setOpen(menu.hidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep parent mobile nav open while interacting with the switcher UI.
|
||||||
|
root.addEventListener("click", function (e) {
|
||||||
|
if (e.target.closest(".lang-switcher__toggle")) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener("click", function (e) {
|
document.addEventListener("click", function (e) {
|
||||||
if (!root.contains(e.target)) setOpen(false);
|
if (!root.contains(e.target)) setOpen(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,15 @@
|
|||||||
|
|
||||||
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
|
<nav id="site-nav" class="nav" aria-label="<%= t('nav.main_menu') %>" aria-hidden="true">
|
||||||
<div class="nav-panel">
|
<div class="nav-panel">
|
||||||
|
<div class="nav-mobile-head">
|
||||||
|
<%= link_to root_path, class: "nav-mobile-brand", aria: { label: "Match Live TV" }, title: "Match Live TV" do %>
|
||||||
|
<img class="nav-mobile-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
|
||||||
|
<span class="brand">Match <span>Live TV</span></span>
|
||||||
|
<% end %>
|
||||||
|
<div class="nav-lang">
|
||||||
|
<%= render "shared/language_switcher" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<%= link_to t("nav.home"), root_path, class: (request.path == "/" ? "nav-active" : nil) %>
|
<%= link_to t("nav.home"), root_path, class: (request.path == "/" ? "nav-active" : nil) %>
|
||||||
<%= link_to t("nav.features"), public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %>
|
<%= link_to t("nav.features"), public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %>
|
||||||
<%= link_to t("nav.pricing"), public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %>
|
<%= link_to t("nav.pricing"), public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %>
|
||||||
@@ -40,9 +49,6 @@
|
|||||||
<%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %>
|
<%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %>
|
||||||
<%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary nav-btn" %>
|
<%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary nav-btn" %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="nav-lang">
|
|
||||||
<%= render "shared/language_switcher" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -78,8 +84,16 @@
|
|||||||
|
|
||||||
if (backdrop) backdrop.addEventListener("click", closeMenu);
|
if (backdrop) backdrop.addEventListener("click", closeMenu);
|
||||||
|
|
||||||
|
function shouldCloseNavOnControl(el) {
|
||||||
|
// Language toggle must keep the mobile menu open; only a locale choice may close it.
|
||||||
|
if (!el.closest("[data-lang-switcher]")) return true;
|
||||||
|
return el.classList.contains("lang-switcher__option");
|
||||||
|
}
|
||||||
|
|
||||||
nav.querySelectorAll("a, button").forEach(function (el) {
|
nav.querySelectorAll("a, button").forEach(function (el) {
|
||||||
el.addEventListener("click", closeMenu);
|
el.addEventListener("click", function () {
|
||||||
|
if (shouldCloseNavOnControl(el)) closeMenu();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("resize", function () {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ de:
|
|||||||
logout_success: Abgemeldet
|
logout_success: Abgemeldet
|
||||||
password_current_incorrect: Das aktuelle Passwort ist falsch
|
password_current_incorrect: Das aktuelle Passwort ist falsch
|
||||||
password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein
|
password_too_short: Das neue Passwort muss mindestens 8 Zeichen lang sein
|
||||||
|
password_too_long: Das neue Passwort darf höchstens 72 Zeichen lang sein
|
||||||
|
password_too_weak: Das neue Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||||
|
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||||
password_mismatch: Die Passwörter stimmen nicht überein
|
password_mismatch: Die Passwörter stimmen nicht überein
|
||||||
password_updated: Passwort aktualisiert
|
password_updated: Passwort aktualisiert
|
||||||
ops_acknowledged: Vorfall übernommen
|
ops_acknowledged: Vorfall übernommen
|
||||||
@@ -53,7 +56,7 @@ de:
|
|||||||
edit:
|
edit:
|
||||||
title: Passwort ändern
|
title: Passwort ändern
|
||||||
current_password_label: Aktuelles Passwort
|
current_password_label: Aktuelles Passwort
|
||||||
new_password_label: "Neues Passwort (mind. 8 Zeichen)"
|
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||||
confirm_password_label: Neues Passwort bestätigen
|
confirm_password_label: Neues Passwort bestätigen
|
||||||
submit: Passwort speichern
|
submit: Passwort speichern
|
||||||
cancel: Abbrechen
|
cancel: Abbrechen
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ en:
|
|||||||
logout_success: Signed out
|
logout_success: Signed out
|
||||||
password_current_incorrect: Current password is incorrect
|
password_current_incorrect: Current password is incorrect
|
||||||
password_too_short: The new password must be at least 8 characters long
|
password_too_short: The new password must be at least 8 characters long
|
||||||
|
password_too_long: New password cannot exceed 72 characters
|
||||||
|
password_too_weak: "New password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||||
|
password_same_as_current: New password must be different from the current password
|
||||||
password_mismatch: Passwords do not match
|
password_mismatch: Passwords do not match
|
||||||
password_updated: Password updated
|
password_updated: Password updated
|
||||||
ops_acknowledged: Incident acknowledged
|
ops_acknowledged: Incident acknowledged
|
||||||
@@ -53,7 +56,7 @@ en:
|
|||||||
edit:
|
edit:
|
||||||
title: Change password
|
title: Change password
|
||||||
current_password_label: Current password
|
current_password_label: Current password
|
||||||
new_password_label: "New password (min. 8 characters)"
|
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||||
confirm_password_label: Confirm new password
|
confirm_password_label: Confirm new password
|
||||||
submit: Save password
|
submit: Save password
|
||||||
cancel: Cancel
|
cancel: Cancel
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ es:
|
|||||||
logout_success: Sesión cerrada
|
logout_success: Sesión cerrada
|
||||||
password_current_incorrect: La contraseña actual no es correcta
|
password_current_incorrect: La contraseña actual no es correcta
|
||||||
password_too_short: La nueva contraseña debe tener al menos 8 caracteres
|
password_too_short: La nueva contraseña debe tener al menos 8 caracteres
|
||||||
|
password_too_long: La nueva contraseña no puede superar los 72 caracteres
|
||||||
|
password_too_weak: "La nueva contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||||
|
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||||
password_mismatch: Las contraseñas no coinciden
|
password_mismatch: Las contraseñas no coinciden
|
||||||
password_updated: Contraseña actualizada
|
password_updated: Contraseña actualizada
|
||||||
ops_acknowledged: Incidencia asumida
|
ops_acknowledged: Incidencia asumida
|
||||||
@@ -53,7 +56,7 @@ es:
|
|||||||
edit:
|
edit:
|
||||||
title: Cambiar contraseña
|
title: Cambiar contraseña
|
||||||
current_password_label: Contraseña actual
|
current_password_label: Contraseña actual
|
||||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||||
confirm_password_label: Confirma la nueva contraseña
|
confirm_password_label: Confirma la nueva contraseña
|
||||||
submit: Guardar contraseña
|
submit: Guardar contraseña
|
||||||
cancel: Cancelar
|
cancel: Cancelar
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ fr:
|
|||||||
logout_success: Déconnecté
|
logout_success: Déconnecté
|
||||||
password_current_incorrect: Le mot de passe actuel est incorrect
|
password_current_incorrect: Le mot de passe actuel est incorrect
|
||||||
password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères
|
password_too_short: Le nouveau mot de passe doit contenir au moins 8 caractères
|
||||||
|
password_too_long: Le nouveau mot de passe ne peut pas dépasser 72 caractères
|
||||||
|
password_too_weak: "Le nouveau mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||||
|
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||||
password_mismatch: Les mots de passe ne correspondent pas
|
password_mismatch: Les mots de passe ne correspondent pas
|
||||||
password_updated: Mot de passe mis à jour
|
password_updated: Mot de passe mis à jour
|
||||||
ops_acknowledged: Incident pris en charge
|
ops_acknowledged: Incident pris en charge
|
||||||
@@ -53,7 +56,7 @@ fr:
|
|||||||
edit:
|
edit:
|
||||||
title: Changer le mot de passe
|
title: Changer le mot de passe
|
||||||
current_password_label: Mot de passe actuel
|
current_password_label: Mot de passe actuel
|
||||||
new_password_label: "Nouveau mot de passe (min. 8 caractères)"
|
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||||
confirm_password_label: Confirmer le nouveau mot de passe
|
confirm_password_label: Confirmer le nouveau mot de passe
|
||||||
submit: Enregistrer le mot de passe
|
submit: Enregistrer le mot de passe
|
||||||
cancel: Annuler
|
cancel: Annuler
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ it:
|
|||||||
logout_success: Disconnesso
|
logout_success: Disconnesso
|
||||||
password_current_incorrect: Password attuale non corretta
|
password_current_incorrect: Password attuale non corretta
|
||||||
password_too_short: La nuova password deve avere almeno 8 caratteri
|
password_too_short: La nuova password deve avere almeno 8 caratteri
|
||||||
|
password_too_long: La nuova password non può superare i 72 caratteri
|
||||||
|
password_too_weak: "La nuova password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||||
|
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||||
password_mismatch: Le password non coincidono
|
password_mismatch: Le password non coincidono
|
||||||
password_updated: Password aggiornata
|
password_updated: Password aggiornata
|
||||||
ops_acknowledged: Incidente preso in carico
|
ops_acknowledged: Incidente preso in carico
|
||||||
@@ -53,7 +56,7 @@ it:
|
|||||||
edit:
|
edit:
|
||||||
title: Cambia password
|
title: Cambia password
|
||||||
current_password_label: Password attuale
|
current_password_label: Password attuale
|
||||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||||
confirm_password_label: Conferma nuova password
|
confirm_password_label: Conferma nuova password
|
||||||
submit: Salva password
|
submit: Salva password
|
||||||
cancel: Annulla
|
cancel: Annulla
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
de:
|
de:
|
||||||
|
password_policy:
|
||||||
|
hint: "Mindestens 8 Zeichen, mit mindestens 3 aus: Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen."
|
||||||
|
activerecord:
|
||||||
|
attributes:
|
||||||
|
user:
|
||||||
|
password: Passwort
|
||||||
|
admin_account:
|
||||||
|
password: Passwort
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
|
||||||
|
too_long: "ist zu lang (höchstens %{count} Zeichen)"
|
||||||
|
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
|
||||||
|
admin_account:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "ist zu kurz (mindestens %{count} Zeichen)"
|
||||||
|
too_long: "ist zu lang (höchstens %{count} Zeichen)"
|
||||||
|
complexity: "muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten"
|
||||||
club:
|
club:
|
||||||
back_to_club: "← Verein"
|
back_to_club: "← Verein"
|
||||||
sport_label: Hauptsportart
|
sport_label: Hauptsportart
|
||||||
@@ -600,10 +622,16 @@ de:
|
|||||||
welcome_back: Willkommen zurück!
|
welcome_back: Willkommen zurück!
|
||||||
invalid_credentials: E-Mail oder Passwort ungültig
|
invalid_credentials: E-Mail oder Passwort ungültig
|
||||||
logged_out: Abgemeldet
|
logged_out: Abgemeldet
|
||||||
|
unauthorized: Nicht autorisiert
|
||||||
|
invalid_token: Ungültiges Token
|
||||||
password_resets:
|
password_resets:
|
||||||
email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts.
|
email_sent: Wenn die E-Mail registriert ist, erhältst du in Kürze einen Link zum Zurücksetzen des Passworts.
|
||||||
invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an.
|
invalid_or_expired_link: Link ungültig oder abgelaufen. Fordere ein neues Zurücksetzen des Passworts an.
|
||||||
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
|
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
|
||||||
|
password_too_short: Das Passwort muss mindestens 8 Zeichen lang sein
|
||||||
|
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
|
||||||
|
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||||
|
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||||
password_mismatch: Die Passwörter stimmen nicht überein
|
password_mismatch: Die Passwörter stimmen nicht überein
|
||||||
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
|
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
|
||||||
accounts:
|
accounts:
|
||||||
@@ -611,8 +639,12 @@ de:
|
|||||||
profile_updated: Profil aktualisiert.
|
profile_updated: Profil aktualisiert.
|
||||||
password_current_incorrect: Das aktuelle Passwort ist falsch
|
password_current_incorrect: Das aktuelle Passwort ist falsch
|
||||||
password_too_short: Das Passwort muss mindestens 8 Zeichen haben
|
password_too_short: Das Passwort muss mindestens 8 Zeichen haben
|
||||||
|
password_too_long: Das Passwort darf höchstens 72 Zeichen lang sein
|
||||||
|
password_too_weak: Das Passwort muss mindestens 3 aus Kleinbuchstaben, Großbuchstaben, Zahlen und Symbolen enthalten
|
||||||
|
password_same_as_current: Das neue Passwort muss sich vom aktuellen unterscheiden
|
||||||
password_mismatch: Die Passwörter stimmen nicht überein
|
password_mismatch: Die Passwörter stimmen nicht überein
|
||||||
password_updated: Passwort aktualisiert.
|
password_updated: Passwort aktualisiert.
|
||||||
|
password_update_failed: Passwort konnte nicht aktualisiert werden
|
||||||
replay:
|
replay:
|
||||||
download_unavailable: Download nicht verfügbar
|
download_unavailable: Download nicht verfügbar
|
||||||
not_available: Replay nicht verfügbar
|
not_available: Replay nicht verfügbar
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
en:
|
en:
|
||||||
|
password_policy:
|
||||||
|
hint: "At least 8 characters, including 3 of: lowercase, uppercase, numbers and symbols."
|
||||||
|
activerecord:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "is too short (minimum is %{count} characters)"
|
||||||
|
too_long: "is too long (maximum is %{count} characters)"
|
||||||
|
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
|
||||||
|
admin_account:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "is too short (minimum is %{count} characters)"
|
||||||
|
too_long: "is too long (maximum is %{count} characters)"
|
||||||
|
complexity: "must include at least 3 of: lowercase letters, uppercase letters, numbers and symbols"
|
||||||
club:
|
club:
|
||||||
back_to_club: "← Club"
|
back_to_club: "← Club"
|
||||||
sport_label: Main sport
|
sport_label: Main sport
|
||||||
@@ -600,10 +617,16 @@ en:
|
|||||||
welcome_back: Welcome back!
|
welcome_back: Welcome back!
|
||||||
invalid_credentials: Invalid email or password
|
invalid_credentials: Invalid email or password
|
||||||
logged_out: Logged out
|
logged_out: Logged out
|
||||||
|
unauthorized: Unauthorized
|
||||||
|
invalid_token: Invalid token
|
||||||
password_resets:
|
password_resets:
|
||||||
email_sent: If the email is registered, you'll receive a password reset link shortly.
|
email_sent: If the email is registered, you'll receive a password reset link shortly.
|
||||||
invalid_or_expired_link: Invalid or expired link. Request a new password reset.
|
invalid_or_expired_link: Invalid or expired link. Request a new password reset.
|
||||||
password_min_length: Password must be at least 8 characters
|
password_min_length: Password must be at least 8 characters
|
||||||
|
password_too_short: Password must be at least 8 characters
|
||||||
|
password_too_long: Password cannot exceed 72 characters
|
||||||
|
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||||
|
password_same_as_current: New password must be different from the current password
|
||||||
password_mismatch: Passwords don't match
|
password_mismatch: Passwords don't match
|
||||||
password_updated: Password updated. You can now log in.
|
password_updated: Password updated. You can now log in.
|
||||||
accounts:
|
accounts:
|
||||||
@@ -611,8 +634,12 @@ en:
|
|||||||
profile_updated: Profile updated.
|
profile_updated: Profile updated.
|
||||||
password_current_incorrect: Current password is incorrect
|
password_current_incorrect: Current password is incorrect
|
||||||
password_too_short: Password must be at least 8 characters
|
password_too_short: Password must be at least 8 characters
|
||||||
|
password_too_long: Password cannot exceed 72 characters
|
||||||
|
password_too_weak: "Password must include at least 3 of: lowercase, uppercase, numbers and symbols"
|
||||||
|
password_same_as_current: New password must be different from the current password
|
||||||
password_mismatch: Passwords do not match
|
password_mismatch: Passwords do not match
|
||||||
password_updated: Password updated.
|
password_updated: Password updated.
|
||||||
|
password_update_failed: Unable to update password
|
||||||
replay:
|
replay:
|
||||||
download_unavailable: Download not available
|
download_unavailable: Download not available
|
||||||
not_available: Replay not available
|
not_available: Replay not available
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
es:
|
es:
|
||||||
|
password_policy:
|
||||||
|
hint: "Mínimo 8 caracteres, con al menos 3 entre: minúsculas, mayúsculas, números y símbolos."
|
||||||
|
activerecord:
|
||||||
|
attributes:
|
||||||
|
user:
|
||||||
|
password: Contraseña
|
||||||
|
admin_account:
|
||||||
|
password: Contraseña
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "es demasiado corta (mínimo %{count} caracteres)"
|
||||||
|
too_long: "es demasiado larga (máximo %{count} caracteres)"
|
||||||
|
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||||
|
admin_account:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "es demasiado corta (mínimo %{count} caracteres)"
|
||||||
|
too_long: "es demasiado larga (máximo %{count} caracteres)"
|
||||||
|
complexity: "debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||||
club:
|
club:
|
||||||
back_to_club: "← Club"
|
back_to_club: "← Club"
|
||||||
sport_label: Deporte principal
|
sport_label: Deporte principal
|
||||||
@@ -600,10 +622,16 @@ es:
|
|||||||
welcome_back: "¡Bienvenido de nuevo!"
|
welcome_back: "¡Bienvenido de nuevo!"
|
||||||
invalid_credentials: Correo o contraseña no válidos
|
invalid_credentials: Correo o contraseña no válidos
|
||||||
logged_out: Sesión cerrada
|
logged_out: Sesión cerrada
|
||||||
|
unauthorized: No autorizado
|
||||||
|
invalid_token: Token no válido
|
||||||
password_resets:
|
password_resets:
|
||||||
email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña.
|
email_sent: Si el correo está registrado, recibirás en breve un enlace para restablecer la contraseña.
|
||||||
invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña.
|
invalid_or_expired_link: Enlace no válido o caducado. Solicita un nuevo restablecimiento de contraseña.
|
||||||
password_min_length: La contraseña debe tener al menos 8 caracteres
|
password_min_length: La contraseña debe tener al menos 8 caracteres
|
||||||
|
password_too_short: La contraseña debe tener al menos 8 caracteres
|
||||||
|
password_too_long: La contraseña no puede superar los 72 caracteres
|
||||||
|
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||||
|
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||||
password_mismatch: Las contraseñas no coinciden
|
password_mismatch: Las contraseñas no coinciden
|
||||||
password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
|
password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
|
||||||
accounts:
|
accounts:
|
||||||
@@ -611,8 +639,12 @@ es:
|
|||||||
profile_updated: Perfil actualizado.
|
profile_updated: Perfil actualizado.
|
||||||
password_current_incorrect: La contraseña actual no es correcta
|
password_current_incorrect: La contraseña actual no es correcta
|
||||||
password_too_short: La contraseña debe tener al menos 8 caracteres
|
password_too_short: La contraseña debe tener al menos 8 caracteres
|
||||||
|
password_too_long: La contraseña no puede superar los 72 caracteres
|
||||||
|
password_too_weak: "La contraseña debe incluir al menos 3 entre: minúsculas, mayúsculas, números y símbolos"
|
||||||
|
password_same_as_current: La nueva contraseña debe ser distinta de la actual
|
||||||
password_mismatch: Las contraseñas no coinciden
|
password_mismatch: Las contraseñas no coinciden
|
||||||
password_updated: Contraseña actualizada.
|
password_updated: Contraseña actualizada.
|
||||||
|
password_update_failed: No se pudo actualizar la contraseña
|
||||||
replay:
|
replay:
|
||||||
download_unavailable: Descarga no disponible
|
download_unavailable: Descarga no disponible
|
||||||
not_available: Repetición no disponible
|
not_available: Repetición no disponible
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
fr:
|
fr:
|
||||||
|
password_policy:
|
||||||
|
hint: "Au moins 8 caractères, avec au moins 3 parmi : minuscules, majuscules, chiffres et symboles."
|
||||||
|
activerecord:
|
||||||
|
attributes:
|
||||||
|
user:
|
||||||
|
password: Mot de passe
|
||||||
|
admin_account:
|
||||||
|
password: Mot de passe
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "est trop court (minimum %{count} caractères)"
|
||||||
|
too_long: "est trop long (maximum %{count} caractères)"
|
||||||
|
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||||
|
admin_account:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "est trop court (minimum %{count} caractères)"
|
||||||
|
too_long: "est trop long (maximum %{count} caractères)"
|
||||||
|
complexity: "doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||||
club:
|
club:
|
||||||
back_to_club: "← Club"
|
back_to_club: "← Club"
|
||||||
sport_label: Sport principal
|
sport_label: Sport principal
|
||||||
@@ -600,10 +622,16 @@ fr:
|
|||||||
welcome_back: Bon retour !
|
welcome_back: Bon retour !
|
||||||
invalid_credentials: E-mail ou mot de passe invalide
|
invalid_credentials: E-mail ou mot de passe invalide
|
||||||
logged_out: Déconnecté
|
logged_out: Déconnecté
|
||||||
|
unauthorized: Non autorisé
|
||||||
|
invalid_token: Jeton invalide
|
||||||
password_resets:
|
password_resets:
|
||||||
email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe.
|
email_sent: Si l'e-mail est enregistré, tu recevras bientôt un lien pour réinitialiser le mot de passe.
|
||||||
invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe.
|
invalid_or_expired_link: Lien invalide ou expiré. Demande une nouvelle réinitialisation du mot de passe.
|
||||||
password_min_length: Le mot de passe doit comporter au moins 8 caractères
|
password_min_length: Le mot de passe doit comporter au moins 8 caractères
|
||||||
|
password_too_short: Le mot de passe doit comporter au moins 8 caractères
|
||||||
|
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
|
||||||
|
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||||
|
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||||
password_mismatch: Les mots de passe ne correspondent pas
|
password_mismatch: Les mots de passe ne correspondent pas
|
||||||
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
|
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
|
||||||
accounts:
|
accounts:
|
||||||
@@ -611,8 +639,12 @@ fr:
|
|||||||
profile_updated: Profil mis à jour.
|
profile_updated: Profil mis à jour.
|
||||||
password_current_incorrect: Le mot de passe actuel est incorrect
|
password_current_incorrect: Le mot de passe actuel est incorrect
|
||||||
password_too_short: Le mot de passe doit contenir au moins 8 caractères
|
password_too_short: Le mot de passe doit contenir au moins 8 caractères
|
||||||
|
password_too_long: Le mot de passe ne peut pas dépasser 72 caractères
|
||||||
|
password_too_weak: "Le mot de passe doit inclure au moins 3 parmi : minuscules, majuscules, chiffres et symboles"
|
||||||
|
password_same_as_current: "Le nouveau mot de passe doit être différent de l'actuel"
|
||||||
password_mismatch: Les mots de passe ne correspondent pas
|
password_mismatch: Les mots de passe ne correspondent pas
|
||||||
password_updated: Mot de passe mis à jour.
|
password_updated: Mot de passe mis à jour.
|
||||||
|
password_update_failed: Impossible de mettre à jour le mot de passe
|
||||||
replay:
|
replay:
|
||||||
download_unavailable: Téléchargement non disponible
|
download_unavailable: Téléchargement non disponible
|
||||||
not_available: Replay non disponible
|
not_available: Replay non disponible
|
||||||
|
|||||||
@@ -1,4 +1,21 @@
|
|||||||
it:
|
it:
|
||||||
|
password_policy:
|
||||||
|
hint: "Minimo 8 caratteri, con almeno 3 tra: minuscole, maiuscole, numeri e simboli."
|
||||||
|
activerecord:
|
||||||
|
errors:
|
||||||
|
models:
|
||||||
|
user:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "è troppo corta (minimo %{count} caratteri)"
|
||||||
|
too_long: "è troppo lunga (massimo %{count} caratteri)"
|
||||||
|
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
|
||||||
|
admin_account:
|
||||||
|
attributes:
|
||||||
|
password:
|
||||||
|
too_short: "è troppo corta (minimo %{count} caratteri)"
|
||||||
|
too_long: "è troppo lunga (massimo %{count} caratteri)"
|
||||||
|
complexity: "deve includere almeno 3 tra: lettere minuscole, maiuscole, numeri e simboli"
|
||||||
club:
|
club:
|
||||||
back_to_club: "← Società"
|
back_to_club: "← Società"
|
||||||
sport_label: Sport principale
|
sport_label: Sport principale
|
||||||
@@ -600,10 +617,19 @@ it:
|
|||||||
welcome_back: Bentornato!
|
welcome_back: Bentornato!
|
||||||
invalid_credentials: Email o password non validi
|
invalid_credentials: Email o password non validi
|
||||||
logged_out: Disconnesso
|
logged_out: Disconnesso
|
||||||
|
unauthorized: Non autorizzato
|
||||||
|
invalid_token: Token non valido
|
||||||
password_resets:
|
password_resets:
|
||||||
email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password.
|
email_sent: Se l'email è registrata, riceverai a breve un link per reimpostare la password.
|
||||||
invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password.
|
invalid_or_expired_link: Link non valido o scaduto. Richiedi un nuovo reset password.
|
||||||
password_min_length: La password deve avere almeno 8 caratteri
|
password_min_length: La password deve avere almeno 8 caratteri
|
||||||
|
password_too_short: La password deve avere almeno 8 caratteri
|
||||||
|
password_too_long: La password non può superare i 72 caratteri
|
||||||
|
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||||
|
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||||
|
password_too_short: La password deve avere almeno 8 caratteri
|
||||||
|
password_too_long: La password non può superare i 72 caratteri
|
||||||
|
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||||
password_mismatch: Le password non coincidono
|
password_mismatch: Le password non coincidono
|
||||||
password_updated: Password aggiornata. Ora puoi accedere.
|
password_updated: Password aggiornata. Ora puoi accedere.
|
||||||
accounts:
|
accounts:
|
||||||
@@ -611,8 +637,14 @@ it:
|
|||||||
profile_updated: Profilo aggiornato.
|
profile_updated: Profilo aggiornato.
|
||||||
password_current_incorrect: La password attuale non è corretta
|
password_current_incorrect: La password attuale non è corretta
|
||||||
password_too_short: La password deve avere almeno 8 caratteri
|
password_too_short: La password deve avere almeno 8 caratteri
|
||||||
|
password_too_long: La password non può superare i 72 caratteri
|
||||||
|
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||||
|
password_same_as_current: La nuova password deve essere diversa da quella attuale
|
||||||
|
password_too_long: La password non può superare i 72 caratteri
|
||||||
|
password_too_weak: "La password deve includere almeno 3 tra: minuscole, maiuscole, numeri e simboli"
|
||||||
password_mismatch: Le password non coincidono
|
password_mismatch: Le password non coincidono
|
||||||
password_updated: Password aggiornata.
|
password_updated: Password aggiornata.
|
||||||
|
password_update_failed: Impossibile aggiornare la password
|
||||||
replay:
|
replay:
|
||||||
download_unavailable: Download non disponibile
|
download_unavailable: Download non disponibile
|
||||||
not_available: Replay non disponibile
|
not_available: Replay non disponibile
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ de:
|
|||||||
password_reset:
|
password_reset:
|
||||||
meta_title: "Neues Passwort — Match Live TV"
|
meta_title: "Neues Passwort — Match Live TV"
|
||||||
title: Neues Passwort wählen
|
title: Neues Passwort wählen
|
||||||
new_password_label: "Neues Passwort (min. 8 Zeichen)"
|
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||||
submit: Passwort speichern
|
submit: Passwort speichern
|
||||||
back_to_login: Zurück zur Anmeldung
|
back_to_login: Zurück zur Anmeldung
|
||||||
invitation:
|
invitation:
|
||||||
@@ -128,7 +128,7 @@ de:
|
|||||||
name_label: Name
|
name_label: Name
|
||||||
role_label: "Rolle: %{role}"
|
role_label: "Rolle: %{role}"
|
||||||
current_password_label: Aktuelles Passwort
|
current_password_label: Aktuelles Passwort
|
||||||
new_password_label: "Neues Passwort (mind. 8 Zeichen)"
|
new_password_label: "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)"
|
||||||
save_profile: Profil speichern
|
save_profile: Profil speichern
|
||||||
save_password: Passwort aktualisieren
|
save_password: Passwort aktualisieren
|
||||||
common:
|
common:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ en:
|
|||||||
password_reset:
|
password_reset:
|
||||||
meta_title: "New password — Match Live TV"
|
meta_title: "New password — Match Live TV"
|
||||||
title: Choose a new password
|
title: Choose a new password
|
||||||
new_password_label: "New password (min. 8 characters)"
|
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||||
submit: Save password
|
submit: Save password
|
||||||
back_to_login: Back to login
|
back_to_login: Back to login
|
||||||
invitation:
|
invitation:
|
||||||
@@ -128,7 +128,7 @@ en:
|
|||||||
name_label: Name
|
name_label: Name
|
||||||
role_label: "Role: %{role}"
|
role_label: "Role: %{role}"
|
||||||
current_password_label: Current password
|
current_password_label: Current password
|
||||||
new_password_label: "New password (min. 8 characters)"
|
new_password_label: "New password (min. 8, at least 3 character types)"
|
||||||
save_profile: Save profile
|
save_profile: Save profile
|
||||||
save_password: Update password
|
save_password: Update password
|
||||||
common:
|
common:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ es:
|
|||||||
password_reset:
|
password_reset:
|
||||||
meta_title: "Nueva contraseña — Match Live TV"
|
meta_title: "Nueva contraseña — Match Live TV"
|
||||||
title: Elige una nueva contraseña
|
title: Elige una nueva contraseña
|
||||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||||
submit: Guardar contraseña
|
submit: Guardar contraseña
|
||||||
back_to_login: Volver al inicio de sesión
|
back_to_login: Volver al inicio de sesión
|
||||||
invitation:
|
invitation:
|
||||||
@@ -128,7 +128,7 @@ es:
|
|||||||
name_label: Nombre
|
name_label: Nombre
|
||||||
role_label: "Rol: %{role}"
|
role_label: "Rol: %{role}"
|
||||||
current_password_label: Contraseña actual
|
current_password_label: Contraseña actual
|
||||||
new_password_label: "Nueva contraseña (mín. 8 caracteres)"
|
new_password_label: "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)"
|
||||||
save_profile: Guardar perfil
|
save_profile: Guardar perfil
|
||||||
save_password: Actualizar contraseña
|
save_password: Actualizar contraseña
|
||||||
common:
|
common:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ fr:
|
|||||||
password_reset:
|
password_reset:
|
||||||
meta_title: "Nouveau mot de passe — Match Live TV"
|
meta_title: "Nouveau mot de passe — Match Live TV"
|
||||||
title: Choisissez un nouveau mot de passe
|
title: Choisissez un nouveau mot de passe
|
||||||
new_password_label: "Nouveau mot de passe (8 caractères min.)"
|
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||||
submit: Enregistrer le mot de passe
|
submit: Enregistrer le mot de passe
|
||||||
back_to_login: Retour à la connexion
|
back_to_login: Retour à la connexion
|
||||||
invitation:
|
invitation:
|
||||||
@@ -128,7 +128,7 @@ fr:
|
|||||||
name_label: Nom
|
name_label: Nom
|
||||||
role_label: "Rôle : %{role}"
|
role_label: "Rôle : %{role}"
|
||||||
current_password_label: Mot de passe actuel
|
current_password_label: Mot de passe actuel
|
||||||
new_password_label: "Nouveau mot de passe (min. 8 caractères)"
|
new_password_label: "Nouveau mot de passe (min. 8, au moins 3 types de caractères)"
|
||||||
save_profile: Enregistrer le profil
|
save_profile: Enregistrer le profil
|
||||||
save_password: Mettre à jour le mot de passe
|
save_password: Mettre à jour le mot de passe
|
||||||
common:
|
common:
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ it:
|
|||||||
password_reset:
|
password_reset:
|
||||||
meta_title: "Nuova password — Match Live TV"
|
meta_title: "Nuova password — Match Live TV"
|
||||||
title: Scegli una nuova password
|
title: Scegli una nuova password
|
||||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||||
submit: Salva password
|
submit: Salva password
|
||||||
back_to_login: Torna al login
|
back_to_login: Torna al login
|
||||||
invitation:
|
invitation:
|
||||||
@@ -128,7 +128,7 @@ it:
|
|||||||
name_label: Nome
|
name_label: Nome
|
||||||
role_label: "Ruolo: %{role}"
|
role_label: "Ruolo: %{role}"
|
||||||
current_password_label: Password attuale
|
current_password_label: Password attuale
|
||||||
new_password_label: "Nuova password (min. 8 caratteri)"
|
new_password_label: "Nuova password (min. 8, almeno 3 tipi di caratteri)"
|
||||||
save_profile: Salva profilo
|
save_profile: Salva profilo
|
||||||
save_password: Aggiorna password
|
save_password: Aggiorna password
|
||||||
common:
|
common:
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ Rails.application.routes.draw do
|
|||||||
patch "password/reset", to: "password_resets#update"
|
patch "password/reset", to: "password_resets#update"
|
||||||
get "account", to: "accounts#show", as: :account
|
get "account", to: "accounts#show", as: :account
|
||||||
patch "account", to: "accounts#update"
|
patch "account", to: "accounts#update"
|
||||||
|
get "account/password", to: redirect("/account"), as: nil
|
||||||
patch "account/password", to: "accounts#update_password", as: :account_password
|
patch "account/password", to: "accounts#update_password", as: :account_password
|
||||||
get "clubs/new", to: "clubs#new", as: :new_club
|
get "clubs/new", to: "clubs#new", as: :new_club
|
||||||
post "clubs", to: "clubs#create"
|
post "clubs", to: "clubs#create"
|
||||||
|
|||||||
+4
-4
@@ -1,18 +1,18 @@
|
|||||||
load Rails.root.join("db/seeds/plans.rb")
|
load Rails.root.join("db/seeds/plans.rb")
|
||||||
|
|
||||||
AdminAccount.find_or_create_by!(username: "admin") do |a|
|
AdminAccount.find_or_create_by!(username: "admin") do |a|
|
||||||
a.password = "admin"
|
a.password = "AdminPass123"
|
||||||
end
|
end
|
||||||
|
|
||||||
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
|
coach = User.find_or_create_by!(email: "coach@matchlivetv.test") do |u|
|
||||||
u.name = "Coach Demo"
|
u.name = "Coach Demo"
|
||||||
u.password = "password123"
|
u.password = "Password123"
|
||||||
u.role = "coach"
|
u.role = "coach"
|
||||||
end
|
end
|
||||||
|
|
||||||
admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
|
admin = User.find_or_create_by!(email: "admin@matchlivetv.test") do |u|
|
||||||
u.name = "Admin"
|
u.name = "Admin"
|
||||||
u.password = "password123"
|
u.password = "Password123"
|
||||||
u.role = "admin"
|
u.role = "admin"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,5 +41,5 @@ match = team.matches.find_or_create_by!(opponent_name: "ASD Eagles Pavia") do |m
|
|||||||
m.phase = "Semifinale"
|
m.phase = "Semifinale"
|
||||||
end
|
end
|
||||||
|
|
||||||
puts "Seed OK: coach@matchlivetv.test / password123"
|
puts "Seed OK: coach@matchlivetv.test / Password123"
|
||||||
puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}"
|
puts "Club: #{club.name}, Team: #{team.name}, Match: #{match.opponent_name}"
|
||||||
|
|||||||
+107
-16
@@ -199,6 +199,17 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
body.nav-menu-open .site-chrome {
|
body.nav-menu-open .site-chrome {
|
||||||
z-index: 1300;
|
z-index: 1300;
|
||||||
}
|
}
|
||||||
|
/* Keep the close control above the full-screen sheet; hide duplicate mast brand. */
|
||||||
|
body.nav-menu-open .site-masthead {
|
||||||
|
z-index: 1320;
|
||||||
|
background: transparent;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
body.nav-menu-open .mast-brand {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.nav-backdrop {
|
.nav-backdrop {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -236,16 +247,50 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.nav-panel {
|
.nav-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
min-height: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 72px 24px 32px;
|
padding: 16px 24px 32px;
|
||||||
padding-top: calc(72px + env(safe-area-inset-top, 0px));
|
padding-top: calc(16px + env(safe-area-inset-top, 0px));
|
||||||
padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
|
padding-bottom: calc(32px + env(safe-area-inset-bottom, 0px));
|
||||||
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.nav-mobile-head {
|
||||||
|
order: -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
padding: 0 52px 16px 0; /* room for the close (X) control on the right */
|
||||||
|
border-bottom: 1px solid #252530;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.nav-mobile-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.nav-mobile-brand:hover { text-decoration: none; opacity: 0.92; }
|
||||||
|
.nav-mobile-brand .brand { font-size: 1.05rem; }
|
||||||
|
.nav-mobile-brand-logo {
|
||||||
|
display: block;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.nav-panel > a,
|
.nav-panel > a,
|
||||||
.nav-panel .nav-link-item {
|
.nav-panel .nav-link-item {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -263,11 +308,13 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
.nav-actions {
|
.nav-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 14px;
|
gap: 0;
|
||||||
margin-top: 24px;
|
margin-top: 8px;
|
||||||
padding: 24px 0 0;
|
padding: 0;
|
||||||
border-top: 1px solid #252530;
|
border-top: none;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
.nav-actions .nav-link-item {
|
.nav-actions .nav-link-item {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -289,10 +336,14 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
.nav-lang {
|
.nav-lang {
|
||||||
margin-left: 0;
|
margin: 0;
|
||||||
margin-top: 4px;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
width: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.nav-lang .lang-switcher__menu {
|
||||||
|
/* Keep the list inside the open mobile sheet */
|
||||||
|
z-index: 1320;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,14 +393,22 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px 20px;
|
gap: 8px 20px;
|
||||||
}
|
}
|
||||||
|
.nav-mobile-head {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.nav-mobile-brand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.nav-lang {
|
||||||
|
order: 2;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
.nav-actions {
|
.nav-actions {
|
||||||
|
order: 1;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px 12px;
|
gap: 8px 12px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.nav-lang {
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
.nav-backdrop { display: none !important; }
|
.nav-backdrop { display: none !important; }
|
||||||
}
|
}
|
||||||
.btn { display: inline-block; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.9rem; border: none; cursor: pointer; text-decoration: none; }
|
.btn { display: inline-block; padding: 10px 18px; border-radius: 8px; font-weight: 700; font-size: 0.9rem; border: none; cursor: pointer; text-decoration: none; }
|
||||||
@@ -1230,7 +1289,22 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.hero-split .hero-cta { flex-direction: column; }
|
.hero-split .hero-cta { flex-direction: column; }
|
||||||
.hero-split .hero-cta .btn { width: 100%; text-align: center; }
|
.hero-split .hero-cta .btn { width: 100%; text-align: center; }
|
||||||
}
|
}
|
||||||
.section { padding: 40px 0; }
|
.section {
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
/* Keep horizontal inset when .section shares a node with .wrap (padding shorthand must not win). */
|
||||||
|
.section.wrap {
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
.table-scroll {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
.section h2 { font-size: 1.6rem; margin: 0 0 20px; text-align: center; }
|
.section h2 { font-size: 1.6rem; margin: 0 0 20px; text-align: center; }
|
||||||
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
|
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
|
||||||
.step { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 22px; }
|
.step { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 22px; }
|
||||||
@@ -1363,7 +1437,7 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.site-footer__legal { flex: 1 1 100%; margin-top: 4px; }
|
.site-footer__legal { flex: 1 1 100%; margin-top: 4px; }
|
||||||
.site-footer__legal p { margin: 0 0 6px; line-height: 1.45; }
|
.site-footer__legal p { margin: 0 0 6px; line-height: 1.45; }
|
||||||
.site-footer__legal p:last-child { margin-bottom: 0; }
|
.site-footer__legal p:last-child { margin-bottom: 0; }
|
||||||
.compare-table { width: 100%; border-collapse: collapse; margin-top: 20px; font-size: 0.9rem; }
|
.compare-table { width: 100%; min-width: 520px; border-collapse: collapse; margin-top: 0; font-size: 0.9rem; }
|
||||||
.compare-table th, .compare-table td { padding: 10px 12px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
.compare-table th, .compare-table td { padding: 10px 12px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
||||||
.compare-table th { color: #aaa; font-weight: 600; }
|
.compare-table th { color: #aaa; font-weight: 600; }
|
||||||
.billing-documents h2 { margin-top: 0; }
|
.billing-documents h2 { margin-top: 0; }
|
||||||
@@ -1372,11 +1446,14 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.card { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
.card { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
||||||
|
|
||||||
.seo-prose { max-width: 720px; margin: 0 auto; color: #bbb; line-height: 1.65; }
|
.seo-prose { max-width: 720px; margin: 0 auto; color: #bbb; line-height: 1.65; }
|
||||||
.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; }
|
.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; text-align: left; }
|
||||||
.seo-prose h2:first-child { margin-top: 0; }
|
.seo-prose h2:first-child { margin-top: 0; }
|
||||||
.seo-prose p { margin: 0 0 14px; }
|
.seo-prose p { margin: 0 0 14px; }
|
||||||
.seo-prose ul { margin: 0 0 16px; padding-left: 1.25rem; }
|
.seo-prose ul { margin: 0 0 16px; padding-left: 1.25rem; }
|
||||||
.seo-prose a { color: #e53935; }
|
.seo-prose a { color: #e53935; }
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.seo-prose h2 { font-size: 1.15rem; line-height: 1.35; }
|
||||||
|
}
|
||||||
.seo-page .seo-lead { color: #aaa; max-width: 640px; line-height: 1.55; margin-bottom: 28px; }
|
.seo-page .seo-lead { color: #aaa; max-width: 640px; line-height: 1.55; margin-bottom: 28px; }
|
||||||
.faq-list { max-width: 720px; margin: 0 auto; }
|
.faq-list { max-width: 720px; margin: 0 auto; }
|
||||||
.faq-item {
|
.faq-item {
|
||||||
@@ -1412,7 +1489,8 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.legal-doc a { color: #e53935; }
|
.legal-doc a { color: #e53935; }
|
||||||
.legal-meta { color: #888; font-size: 0.88rem; margin-bottom: 24px; }
|
.legal-meta { color: #888; font-size: 0.88rem; margin-bottom: 24px; }
|
||||||
.legal-back { margin-top: 32px; }
|
.legal-back { margin-top: 32px; }
|
||||||
.legal-table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; font-size: 0.88rem; }
|
.legal-doc .table-scroll { margin: 12px 0 16px; }
|
||||||
|
.legal-table { width: 100%; min-width: 560px; border-collapse: collapse; margin: 0; font-size: 0.88rem; }
|
||||||
.legal-table th, .legal-table td { border: 1px solid #2a2a36; padding: 10px 12px; text-align: left; vertical-align: top; }
|
.legal-table th, .legal-table td { border: 1px solid #2a2a36; padding: 10px 12px; text-align: left; vertical-align: top; }
|
||||||
.legal-table th { background: #14141c; color: #ccc; }
|
.legal-table th { background: #14141c; color: #ccc; }
|
||||||
.legal-accept {
|
.legal-accept {
|
||||||
@@ -1447,6 +1525,19 @@ body.nav-menu-open { overflow: hidden; }
|
|||||||
.auth-forgot a { color: #e53935; }
|
.auth-forgot a { color: #e53935; }
|
||||||
table.data { width: 100%; border-collapse: collapse; }
|
table.data { width: 100%; border-collapse: collapse; }
|
||||||
table.data th, table.data td { padding: 8px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
table.data th, table.data td { padding: 8px; border-bottom: 1px solid #2a2a36; text-align: left; }
|
||||||
|
/* Wide roster/match tables: scroll inside the card instead of expanding the page. */
|
||||||
|
@media (max-width: 899px) {
|
||||||
|
.card:has(table.data),
|
||||||
|
.team-streaming-staff:has(table.data),
|
||||||
|
.billing-documents:has(table.data) {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
table.data {
|
||||||
|
min-width: 520px;
|
||||||
|
}
|
||||||
|
}
|
||||||
input, select {
|
input, select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
require "rails_helper"
|
||||||
|
|
||||||
|
RSpec.describe PasswordComplexity do
|
||||||
|
describe ".violation" do
|
||||||
|
it "accepts a password with 3 character classes" do
|
||||||
|
expect(described_class.violation("NewPass123")).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "accepts symbols instead of one letter class" do
|
||||||
|
expect(described_class.violation("newpass1!")).to be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects passwords that are too short" do
|
||||||
|
expect(described_class.violation("Ab1!")).to eq(:too_short)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects passwords with fewer than 3 classes" do
|
||||||
|
expect(described_class.violation("password123")).to eq(:too_weak)
|
||||||
|
expect(described_class.violation("PASSWORD123")).to eq(:too_weak)
|
||||||
|
expect(described_class.violation("Password")).to eq(:too_weak)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".same_as_current?" do
|
||||||
|
let!(:user) { User.create!(email: "same@example.com", name: "Same", password: "Password123", role: "coach") }
|
||||||
|
|
||||||
|
it "detects when the new password matches the current one" do
|
||||||
|
expect(described_class.same_as_current?(user, "Password123")).to eq(true)
|
||||||
|
expect(described_class.same_as_current?(user, "OtherPass123")).to eq(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "User validation" do
|
||||||
|
it "blocks weak passwords on create" do
|
||||||
|
user = User.new(email: "weak@example.com", name: "Weak", password: "password123", role: "coach")
|
||||||
|
expect(user).not_to be_valid
|
||||||
|
expect(user.errors[:password]).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows strong passwords on create" do
|
||||||
|
user = User.new(email: "strong@example.com", name: "Strong", password: "NewPass123", role: "coach")
|
||||||
|
expect(user).to be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe "Account API", type: :request do
|
RSpec.describe "Account API", type: :request do
|
||||||
let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") }
|
let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "Password123", role: "coach") }
|
||||||
let(:auth_headers) do
|
let(:auth_headers) do
|
||||||
post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
|
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||||
token = JSON.parse(response.body).fetch("access_token")
|
token = JSON.parse(response.body).fetch("access_token")
|
||||||
{ "Authorization" => "Bearer #{token}" }
|
{ "Authorization" => "Bearer #{token}" }
|
||||||
end
|
end
|
||||||
@@ -40,29 +40,67 @@ RSpec.describe "Account API", type: :request do
|
|||||||
it "changes the password with the current password" do
|
it "changes the password with the current password" do
|
||||||
patch "/api/v1/account/password",
|
patch "/api/v1/account/password",
|
||||||
params: {
|
params: {
|
||||||
current_password: "password123",
|
current_password: "Password123",
|
||||||
password: "newpass123",
|
password: "NewPass123",
|
||||||
password_confirmation: "newpass123"
|
password_confirmation: "NewPass123"
|
||||||
},
|
},
|
||||||
headers: auth_headers
|
headers: auth_headers
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(user.reload.authenticate("newpass123")).to be_truthy
|
expect(user.reload.authenticate("NewPass123")).to be_truthy
|
||||||
end
|
end
|
||||||
|
|
||||||
it "rejects an incorrect current password" do
|
it "rejects an incorrect current password" do
|
||||||
patch "/api/v1/account/password",
|
patch "/api/v1/account/password",
|
||||||
params: {
|
params: {
|
||||||
current_password: "wrong",
|
current_password: "wrong",
|
||||||
password: "newpass123",
|
password: "NewPass123",
|
||||||
password_confirmation: "newpass123"
|
password_confirmation: "NewPass123"
|
||||||
},
|
},
|
||||||
headers: auth_headers
|
headers: auth_headers
|
||||||
expect(response).to have_http_status(:unprocessable_entity)
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
expect(user.reload.authenticate("password123")).to be_truthy
|
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects a password that fails complexity rules" do
|
||||||
|
patch "/api/v1/account/password",
|
||||||
|
params: {
|
||||||
|
current_password: "Password123",
|
||||||
|
password: "newpass123",
|
||||||
|
password_confirmation: "newpass123"
|
||||||
|
},
|
||||||
|
headers: auth_headers.merge("Accept-Language" => "en")
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(JSON.parse(response.body)["error"]).to match(/3 of/i)
|
||||||
|
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "rejects reusing the current password" do
|
||||||
|
patch "/api/v1/account/password",
|
||||||
|
params: {
|
||||||
|
current_password: "Password123",
|
||||||
|
password: "Password123",
|
||||||
|
password_confirmation: "Password123"
|
||||||
|
},
|
||||||
|
headers: auth_headers.merge("Accept-Language" => "en")
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(JSON.parse(response.body)["error"]).to match(/different/i)
|
||||||
|
expect(user.reload.authenticate("Password123")).to be_truthy
|
||||||
|
end
|
||||||
|
|
||||||
|
it "localizes password errors from Accept-Language" do
|
||||||
|
patch "/api/v1/account/password",
|
||||||
|
params: {
|
||||||
|
current_password: "Password123",
|
||||||
|
password: "Password123",
|
||||||
|
password_confirmation: "Password123"
|
||||||
|
},
|
||||||
|
headers: auth_headers.merge("Accept-Language" => "it")
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(JSON.parse(response.body)["error"]).to eq("La nuova password deve essere diversa da quella attuale")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "POST /api/v1/auth/password/forgot" do
|
describe "POST /api/v1/auth/password/forgot" do
|
||||||
it "always returns ok and sends mail when the user exists" do
|
it "always returns ok and sends mail when the user exists" do
|
||||||
expect {
|
expect {
|
||||||
post "/api/v1/auth/password/forgot", params: { email: user.email }
|
post "/api/v1/auth/password/forgot", params: { email: user.email }
|
||||||
@@ -77,5 +115,13 @@ RSpec.describe "Account API", type: :request do
|
|||||||
}.not_to change { ActionMailer::Base.deliveries.size }
|
}.not_to change { ActionMailer::Base.deliveries.size }
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "localizes the response message from Accept-Language" do
|
||||||
|
post "/api/v1/auth/password/forgot",
|
||||||
|
params: { email: user.email },
|
||||||
|
headers: { "Accept-Language" => "fr" }
|
||||||
|
expect(response).to have_http_status(:ok)
|
||||||
|
expect(JSON.parse(response.body)["message"]).to include("réinitialiser")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
RSpec.describe "Auth API", type: :request do
|
RSpec.describe "Auth API", type: :request do
|
||||||
let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "password123", role: "coach") }
|
let!(:user) { User.create!(email: "test@example.com", name: "Test", password: "Password123", role: "coach") }
|
||||||
|
|
||||||
it "logs in with valid credentials" do
|
it "logs in with valid credentials" do
|
||||||
post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
|
post "/api/v1/auth/login", params: { email: user.email, password: "Password123" }
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response).to have_http_status(:ok)
|
||||||
expect(JSON.parse(response.body)).to have_key("access_token")
|
expect(JSON.parse(response.body)).to have_key("access_token")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
# iOS — allineamento password policy + errori API localizzati
|
||||||
|
|
||||||
|
Documento operativo per **Cursor su Mac**: allineare l’app iOS a backend/Android dopo il ramo `feature/password-policy` (merge su `main`).
|
||||||
|
|
||||||
|
**Aggiornato:** 2026-08-08
|
||||||
|
**Android di riferimento (testato su emulatore → API locale):** `2.0.10-native` (`versionCode` **31**)
|
||||||
|
**iOS oggi:** marketing `2.0.5` / build `26`
|
||||||
|
**API produzione:** `https://www.matchlivetv.it`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contesto (già in produzione backend dopo questo rilascio)
|
||||||
|
|
||||||
|
### Policy password (server)
|
||||||
|
|
||||||
|
Definita in `backend/app/models/concerns/password_complexity.rb`:
|
||||||
|
|
||||||
|
- minimo **8** caratteri, massimo **72** byte (bcrypt)
|
||||||
|
- almeno **3 classi su 4**: minuscole, maiuscole, numeri, simboli
|
||||||
|
- in **cambio password** e **reset**: la nuova password **non** può coincidere con quella attuale
|
||||||
|
|
||||||
|
Validazione ActiveModel su `User` / `AdminAccount`. Endpoint che la applicano:
|
||||||
|
|
||||||
|
| Endpoint | Note |
|
||||||
|
|----------|------|
|
||||||
|
| `PATCH /api/v1/account/password` | App native + stessi codici errore |
|
||||||
|
| `PATCH /account/password` (web) | Flash I18n |
|
||||||
|
| reset password pubblico | Stesse regole |
|
||||||
|
| registrazione (`auth/register`) | Validazione modello |
|
||||||
|
|
||||||
|
### Errori API localizzati
|
||||||
|
|
||||||
|
`ApplicationController#set_api_locale` legge, in ordine:
|
||||||
|
|
||||||
|
1. header `X-Locale`
|
||||||
|
2. `Accept-Language`
|
||||||
|
3. `I18n.default_locale` (fallback; **retrocompatibile** se l’app vecchia non manda nulla)
|
||||||
|
|
||||||
|
Risposta invariata: `{ "error": "<messaggio già tradotto>" }` (o `{ "message": "..." }` su alcuni successi).
|
||||||
|
|
||||||
|
Chiavi rilevanti (`backend/config/locales/app.{it,en,fr,de,es}.yml`):
|
||||||
|
|
||||||
|
- `flash.accounts.password_too_short`
|
||||||
|
- `flash.accounts.password_too_long`
|
||||||
|
- `flash.accounts.password_too_weak`
|
||||||
|
- `flash.accounts.password_same_as_current`
|
||||||
|
- `flash.accounts.password_current_incorrect`
|
||||||
|
- `flash.accounts.password_mismatch`
|
||||||
|
- `flash.accounts.password_updated` / `name_required`
|
||||||
|
- analoghi in `flash.password_resets.*` e `flash.sessions.*`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stato iOS vs Android (dopo il merge di questo branch)
|
||||||
|
|
||||||
|
| Area | Android 2.0.10 | iOS (repo dopo merge) | Da fare su Mac |
|
||||||
|
|------|----------------|------------------------|----------------|
|
||||||
|
| Header `Accept-Language` su tutte le request API | `AppContainer` OkHttp interceptor | `MatchLiveAPI.request` + `multipartPatch` | **Verificare** in debug che parta su login/account/password; se manca su altri helper HTTP, allineare |
|
||||||
|
| Hint UI nuova password | `account_new_password` in `values*` (5 lingue) | `account.new.password` in `AppLanguage.swift` (già allineato al testo policy) | OK — rivedi solo se cambi copy |
|
||||||
|
| Schermata Account | Mostra `error` dal body HTTP | `AccountScreen` + `APIError` estrae `error`/`message` | **Test manuale** (vedi sotto) |
|
||||||
|
| Parsing errori API | Regex su `"error"` | `APIError.friendlyHttpMessage` | OK |
|
||||||
|
| Client-side pre-check complessità | No (si affida al server) | No | Opzionale: stessa regola di `PasswordComplexity` prima della PATCH |
|
||||||
|
| Versione App Store | `2.0.10-native` / 31 | `2.0.5` / 26 | **Bump** marketing + build prima del submit TestFlight/App Store |
|
||||||
|
| Signup in-app | Non è il flusso principale | Nessuna UI register dedicata | N/A (signup resta web) |
|
||||||
|
|
||||||
|
### File già toccati / da tenere
|
||||||
|
|
||||||
|
```
|
||||||
|
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # Accept-Language
|
||||||
|
native/ios/MatchLiveTv/Core/AppLanguage.swift # hint account.new.password (5 lingue)
|
||||||
|
native/ios/MatchLiveTv/UI/Account/AccountScreen.swift
|
||||||
|
native/ios/MatchLiveTv/Core/UserFacingError.swift
|
||||||
|
native/ios/MatchLiveTv/Data/API/MatchLiveAPI.swift # enum APIError
|
||||||
|
```
|
||||||
|
|
||||||
|
Riferimento Android:
|
||||||
|
|
||||||
|
```
|
||||||
|
native/android/.../data/AppContainer.kt
|
||||||
|
native/android/.../ui/account/AccountScreen.kt
|
||||||
|
native/android/app/src/main/res/values*/strings.xml # account_new_password
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist operativa su Mac
|
||||||
|
|
||||||
|
1. **Pull** `main` (dopo merge/push da Linux).
|
||||||
|
2. Apri `native/ios/MatchLiveTv.xcodeproj`.
|
||||||
|
3. Build debug con `API_BASE_URL=https://www.matchlivetv.it` (o locale se hai tunnel).
|
||||||
|
4. **Test Account** (credenziali demo se disponibili, altrimenti account reale di test):
|
||||||
|
- password debole (`password`) → messaggio *too_weak* nella lingua UI
|
||||||
|
- password = corrente → *same_as_current*
|
||||||
|
- conferma diversa → *mismatch*
|
||||||
|
- password valida nuova → successo; login successivo ok
|
||||||
|
5. Cambia lingua app (globo) e ripeti un errore: il testo API deve seguire `Accept-Language` / lingua scelta.
|
||||||
|
6. **Bump versione** allineata ad Android se rilasci store: es. marketing `2.0.10`, build `≥ 31`.
|
||||||
|
7. (Opzionale) Unit test Swift che replica `PasswordComplexity.strong_enough?` se aggiungi validazione client.
|
||||||
|
|
||||||
|
### Comandi utili
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone/pull
|
||||||
|
git checkout main && git pull
|
||||||
|
|
||||||
|
# Release iOS (script repo)
|
||||||
|
./scripts/build_ios_release.sh
|
||||||
|
# oppure API staging:
|
||||||
|
# API_BASE_URL=https://www.matchlivetv.it ./scripts/build_ios_release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cosa non serve rifare su iOS
|
||||||
|
|
||||||
|
- Fix CSS/menu mobile web, tabelle scroll, logo nel drawer: **solo sito**.
|
||||||
|
- Redirect `GET /account/password` → `/account`: **solo web**.
|
||||||
|
- Seed password / concern Rails: già lato server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Retrocompatibilità (per release iOS)
|
||||||
|
|
||||||
|
- App iOS **vecchie** (senza `Accept-Language`): continuano a funzionare; messaggi API nella locale default del server.
|
||||||
|
- Login con password già esistenti: invariato.
|
||||||
|
- Solo **impostazione/cambio** password debole viene rifiutato (stesso JSON `error`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Criterio “fatto”
|
||||||
|
|
||||||
|
- [ ] `Accept-Language` presente su login, me, account, change password, forgot (Charles/Proxyman o log)
|
||||||
|
- [ ] Errori password in it/en/fr/de/es coerenti col backend
|
||||||
|
- [ ] Hint campo nuova password parla di “min. 8 / 3 tipi”
|
||||||
|
- [ ] Versione bumpata se lo store release è in scope
|
||||||
|
- [ ] Nessuna regressione login / lista partite / broadcast
|
||||||
|
|
||||||
|
Quando completo, aggiorna questo file con data + versione iOS rilasciata.
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
*.iml
|
*.iml
|
||||||
/captures/
|
/captures/
|
||||||
/app/build/
|
/app/build/
|
||||||
|
/dist/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
keystore.properties
|
keystore.properties
|
||||||
keystore/
|
keystore/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.matchlivetv.match_live_tv.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.matchlivetv.match_live_tv.core.AppConfig
|
import com.matchlivetv.match_live_tv.core.AppConfig
|
||||||
|
import com.matchlivetv.match_live_tv.core.AppLocale
|
||||||
import com.matchlivetv.match_live_tv.core.TokenStore
|
import com.matchlivetv.match_live_tv.core.TokenStore
|
||||||
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
|
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
|
||||||
import com.matchlivetv.match_live_tv.data.cable.SessionCableService
|
import com.matchlivetv.match_live_tv.data.cable.SessionCableService
|
||||||
@@ -37,10 +38,20 @@ class AppContainer(context: Context) {
|
|||||||
accessToken?.let { header("Authorization", "Bearer $it") }
|
accessToken?.let { header("Authorization", "Bearer $it") }
|
||||||
header("Accept", "application/json")
|
header("Accept", "application/json")
|
||||||
header("Content-Type", "application/json")
|
header("Content-Type", "application/json")
|
||||||
|
header("Accept-Language", apiLanguageTag())
|
||||||
}.build()
|
}.build()
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun apiLanguageTag(): String {
|
||||||
|
val tag = AppLocale.currentTag(appContext)
|
||||||
|
return if (tag.isBlank()) {
|
||||||
|
java.util.Locale.getDefault().language.ifBlank { "it" }
|
||||||
|
} else {
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val okHttp = OkHttpClient.Builder()
|
private val okHttp = OkHttpClient.Builder()
|
||||||
.connectTimeout(30, TimeUnit.SECONDS)
|
.connectTimeout(30, TimeUnit.SECONDS)
|
||||||
.readTimeout(60, TimeUnit.SECONDS)
|
.readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ Dies kann nicht rückgängig gemacht werden.</string>
|
|||||||
<string name="account_name_label">Name</string>
|
<string name="account_name_label">Name</string>
|
||||||
<string name="account_role">Rolle: %1$s</string>
|
<string name="account_role">Rolle: %1$s</string>
|
||||||
<string name="account_current_password">Aktuelles Passwort</string>
|
<string name="account_current_password">Aktuelles Passwort</string>
|
||||||
<string name="account_new_password">Neues Passwort (mind. 8 Zeichen)</string>
|
<string name="account_new_password">Neues Passwort (mind. 8, mindestens 3 Zeichenarten)</string>
|
||||||
<string name="account_confirm_password">Passwort bestätigen</string>
|
<string name="account_confirm_password">Passwort bestätigen</string>
|
||||||
<string name="account_save_profile">Profil speichern</string>
|
<string name="account_save_profile">Profil speichern</string>
|
||||||
<string name="account_save_password">Passwort aktualisieren</string>
|
<string name="account_save_password">Passwort aktualisieren</string>
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ This cannot be undone.</string>
|
|||||||
<string name="account_name_label">Name</string>
|
<string name="account_name_label">Name</string>
|
||||||
<string name="account_role">Role: %1$s</string>
|
<string name="account_role">Role: %1$s</string>
|
||||||
<string name="account_current_password">Current password</string>
|
<string name="account_current_password">Current password</string>
|
||||||
<string name="account_new_password">New password (min. 8 characters)</string>
|
<string name="account_new_password">New password (min. 8, at least 3 character types)</string>
|
||||||
<string name="account_confirm_password">Confirm password</string>
|
<string name="account_confirm_password">Confirm password</string>
|
||||||
<string name="account_save_profile">Save profile</string>
|
<string name="account_save_profile">Save profile</string>
|
||||||
<string name="account_save_password">Update password</string>
|
<string name="account_save_password">Update password</string>
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ Esta acción no se puede deshacer.</string>
|
|||||||
<string name="account_name_label">Nombre</string>
|
<string name="account_name_label">Nombre</string>
|
||||||
<string name="account_role">Rol: %1$s</string>
|
<string name="account_role">Rol: %1$s</string>
|
||||||
<string name="account_current_password">Contraseña actual</string>
|
<string name="account_current_password">Contraseña actual</string>
|
||||||
<string name="account_new_password">Nueva contraseña (mín. 8 caracteres)</string>
|
<string name="account_new_password">Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)</string>
|
||||||
<string name="account_confirm_password">Confirmar contraseña</string>
|
<string name="account_confirm_password">Confirmar contraseña</string>
|
||||||
<string name="account_save_profile">Guardar perfil</string>
|
<string name="account_save_profile">Guardar perfil</string>
|
||||||
<string name="account_save_password">Actualizar contraseña</string>
|
<string name="account_save_password">Actualizar contraseña</string>
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ Cette action est irréversible.</string>
|
|||||||
<string name="account_name_label">Nom</string>
|
<string name="account_name_label">Nom</string>
|
||||||
<string name="account_role">Rôle : %1$s</string>
|
<string name="account_role">Rôle : %1$s</string>
|
||||||
<string name="account_current_password">Mot de passe actuel</string>
|
<string name="account_current_password">Mot de passe actuel</string>
|
||||||
<string name="account_new_password">Nouveau mot de passe (min. 8 caractères)</string>
|
<string name="account_new_password">Nouveau mot de passe (min. 8, au moins 3 types de caractères)</string>
|
||||||
<string name="account_confirm_password">Confirmer le mot de passe</string>
|
<string name="account_confirm_password">Confirmer le mot de passe</string>
|
||||||
<string name="account_save_profile">Enregistrer le profil</string>
|
<string name="account_save_profile">Enregistrer le profil</string>
|
||||||
<string name="account_save_password">Mettre à jour le mot de passe</string>
|
<string name="account_save_password">Mettre à jour le mot de passe</string>
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ L\'operazione non si può annullare.</string>
|
|||||||
<string name="account_name_label">Nome</string>
|
<string name="account_name_label">Nome</string>
|
||||||
<string name="account_role">Ruolo: %1$s</string>
|
<string name="account_role">Ruolo: %1$s</string>
|
||||||
<string name="account_current_password">Password attuale</string>
|
<string name="account_current_password">Password attuale</string>
|
||||||
<string name="account_new_password">Nuova password (min. 8 caratteri)</string>
|
<string name="account_new_password">Nuova password (min. 8, almeno 3 tipi di caratteri)</string>
|
||||||
<string name="account_confirm_password">Conferma password</string>
|
<string name="account_confirm_password">Conferma password</string>
|
||||||
<string name="account_save_profile">Salva profilo</string>
|
<string name="account_save_profile">Salva profilo</string>
|
||||||
<string name="account_save_password">Aggiorna password</string>
|
<string name="account_save_password">Aggiorna password</string>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ enum L10n {
|
|||||||
"account.confirm.password": "Conferma password",
|
"account.confirm.password": "Conferma password",
|
||||||
"account.current.password": "Password attuale",
|
"account.current.password": "Password attuale",
|
||||||
"account.name.label": "Nome",
|
"account.name.label": "Nome",
|
||||||
"account.new.password": "Nuova password (min. 8 caratteri)",
|
"account.new.password": "Nuova password (min. 8, almeno 3 tipi di caratteri)",
|
||||||
"account.password.heading": "Cambia password",
|
"account.password.heading": "Cambia password",
|
||||||
"account.password.saved": "Password aggiornata",
|
"account.password.saved": "Password aggiornata",
|
||||||
"account.profile.heading": "Profilo",
|
"account.profile.heading": "Profilo",
|
||||||
@@ -372,7 +372,7 @@ enum L10n {
|
|||||||
"account.confirm.password": "Confirm password",
|
"account.confirm.password": "Confirm password",
|
||||||
"account.current.password": "Current password",
|
"account.current.password": "Current password",
|
||||||
"account.name.label": "Name",
|
"account.name.label": "Name",
|
||||||
"account.new.password": "New password (min. 8 characters)",
|
"account.new.password": "New password (min. 8, at least 3 character types)",
|
||||||
"account.password.heading": "Change password",
|
"account.password.heading": "Change password",
|
||||||
"account.password.saved": "Password updated",
|
"account.password.saved": "Password updated",
|
||||||
"account.profile.heading": "Profile",
|
"account.profile.heading": "Profile",
|
||||||
@@ -663,7 +663,7 @@ enum L10n {
|
|||||||
"account.confirm.password": "Confirmer le mot de passe",
|
"account.confirm.password": "Confirmer le mot de passe",
|
||||||
"account.current.password": "Mot de passe actuel",
|
"account.current.password": "Mot de passe actuel",
|
||||||
"account.name.label": "Nom",
|
"account.name.label": "Nom",
|
||||||
"account.new.password": "Nouveau mot de passe (min. 8 caractères)",
|
"account.new.password": "Nouveau mot de passe (min. 8, au moins 3 types de caractères)",
|
||||||
"account.password.heading": "Changer le mot de passe",
|
"account.password.heading": "Changer le mot de passe",
|
||||||
"account.password.saved": "Mot de passe mis à jour",
|
"account.password.saved": "Mot de passe mis à jour",
|
||||||
"account.profile.heading": "Profil",
|
"account.profile.heading": "Profil",
|
||||||
@@ -954,7 +954,7 @@ enum L10n {
|
|||||||
"account.confirm.password": "Passwort bestätigen",
|
"account.confirm.password": "Passwort bestätigen",
|
||||||
"account.current.password": "Aktuelles Passwort",
|
"account.current.password": "Aktuelles Passwort",
|
||||||
"account.name.label": "Name",
|
"account.name.label": "Name",
|
||||||
"account.new.password": "Neues Passwort (mind. 8 Zeichen)",
|
"account.new.password": "Neues Passwort (mind. 8, mindestens 3 Zeichenarten)",
|
||||||
"account.password.heading": "Passwort ändern",
|
"account.password.heading": "Passwort ändern",
|
||||||
"account.password.saved": "Passwort aktualisiert",
|
"account.password.saved": "Passwort aktualisiert",
|
||||||
"account.profile.heading": "Profil",
|
"account.profile.heading": "Profil",
|
||||||
@@ -1245,7 +1245,7 @@ enum L10n {
|
|||||||
"account.confirm.password": "Confirmar contraseña",
|
"account.confirm.password": "Confirmar contraseña",
|
||||||
"account.current.password": "Contraseña actual",
|
"account.current.password": "Contraseña actual",
|
||||||
"account.name.label": "Nombre",
|
"account.name.label": "Nombre",
|
||||||
"account.new.password": "Nueva contraseña (mín. 8 caracteres)",
|
"account.new.password": "Nueva contraseña (mín. 8, al menos 3 tipos de caracteres)",
|
||||||
"account.password.heading": "Cambiar contraseña",
|
"account.password.heading": "Cambiar contraseña",
|
||||||
"account.password.saved": "Contraseña actualizada",
|
"account.password.saved": "Contraseña actualizada",
|
||||||
"account.profile.heading": "Perfil",
|
"account.profile.heading": "Perfil",
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ final class MatchLiveAPI: @unchecked Sendable {
|
|||||||
request.httpMethod = "PATCH"
|
request.httpMethod = "PATCH"
|
||||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
|
||||||
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
|
if let accessToken { request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") }
|
||||||
request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary)
|
request.httpBody = MultipartBuilder.build(fields: fields, boundary: boundary)
|
||||||
return try await execute(request)
|
return try await execute(request)
|
||||||
@@ -253,6 +254,7 @@ final class MatchLiveAPI: @unchecked Sendable {
|
|||||||
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
var request = URLRequest(url: baseURL.appendingPathComponent(path))
|
||||||
request.httpMethod = method
|
request.httpMethod = method
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||||
|
request.setValue(AppLanguage.resolvedCode, forHTTPHeaderField: "Accept-Language")
|
||||||
if body != nil {
|
if body != nil {
|
||||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
request.httpBody = try ApiInstant.encoder.encode(body)
|
request.httpBody = try ApiInstant.encoder.encode(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user