+ <%= t("auth.account.title") %>
+
+
+
<%= t("auth.account.profile_heading") %>
+ <%= form_with url: public_account_path, method: :patch, local: true do %>
+
+
+
+
+
+ <%= t("auth.account.role_label", role: current_user.role) %>
+
+ <%= render "shared/input_toggle",
+ name: :name,
+ label: t("auth.account.name_label"),
+ value: current_user.name,
+ required: true,
+ autocomplete: "name",
+ masked: false %>
+ <%= submit_tag t("auth.account.save_profile"), class: "btn btn-primary" %>
+ <% end %>
+
+
+
+
<%= t("auth.account.password_heading") %>
+ <%= form_with url: public_account_password_path, method: :patch, local: true do %>
+ <%= render "shared/input_toggle",
+ name: :current_password,
+ label: t("auth.account.current_password_label"),
+ input_type: "password",
+ required: true,
+ autocomplete: "current-password" %>
+ <%= render "shared/input_toggle",
+ name: :password,
+ label: t("auth.account.new_password_label"),
+ input_type: "password",
+ required: true,
+ autocomplete: "new-password" %>
+ <%= render "shared/input_toggle",
+ name: :password_confirmation,
+ label: t("auth.password_confirmation"),
+ input_type: "password",
+ required: true,
+ autocomplete: "new-password" %>
+ <%= submit_tag t("auth.account.save_password"), class: "btn btn-primary" %>
+ <% end %>
+
+
diff --git a/backend/app/views/shared/_marketing_nav.html.erb b/backend/app/views/shared/_marketing_nav.html.erb
index 0edcee8..1a34c10 100644
--- a/backend/app/views/shared/_marketing_nav.html.erb
+++ b/backend/app/views/shared/_marketing_nav.html.erb
@@ -34,6 +34,7 @@
<%= link_to t("nav.my_team"), public_team_details_path(current_user.manageable_teams.first), class: "nav-link-item" %>
<% end %>
<% end %>
+ <%= link_to t("nav.account"), public_account_path, class: (request.path == "/account" ? "nav-link-item nav-active" : "nav-link-item") %>
<%= button_to t("nav.logout"), public_logout_path, method: :delete, class: "btn btn-secondary nav-btn" %>
<% else %>
<%= link_to t("nav.login"), public_login_path, class: "nav-link-item" %>
diff --git a/backend/config/locales/app.de.yml b/backend/config/locales/app.de.yml
index 1983571..68b778e 100644
--- a/backend/config/locales/app.de.yml
+++ b/backend/config/locales/app.de.yml
@@ -606,6 +606,13 @@ de:
password_min_length: Das Passwort muss mindestens 8 Zeichen lang sein
password_mismatch: Die Passwörter stimmen nicht überein
password_updated: Passwort aktualisiert. Du kannst dich jetzt anmelden.
+ accounts:
+ name_required: Der Name ist erforderlich
+ profile_updated: Profil aktualisiert.
+ password_current_incorrect: Das aktuelle Passwort ist falsch
+ password_too_short: Das Passwort muss mindestens 8 Zeichen haben
+ password_mismatch: Die Passwörter stimmen nicht überein
+ password_updated: Passwort aktualisiert.
replay:
download_unavailable: Download nicht verfügbar
not_available: Replay nicht verfügbar
diff --git a/backend/config/locales/app.en.yml b/backend/config/locales/app.en.yml
index 831d0b1..b5c9355 100644
--- a/backend/config/locales/app.en.yml
+++ b/backend/config/locales/app.en.yml
@@ -606,6 +606,13 @@ en:
password_min_length: Password must be at least 8 characters
password_mismatch: Passwords don't match
password_updated: Password updated. You can now log in.
+ accounts:
+ name_required: Name is required
+ profile_updated: Profile updated.
+ password_current_incorrect: Current password is incorrect
+ password_too_short: Password must be at least 8 characters
+ password_mismatch: Passwords do not match
+ password_updated: Password updated.
replay:
download_unavailable: Download not available
not_available: Replay not available
diff --git a/backend/config/locales/app.es.yml b/backend/config/locales/app.es.yml
index c21b77a..d1d984b 100644
--- a/backend/config/locales/app.es.yml
+++ b/backend/config/locales/app.es.yml
@@ -606,6 +606,13 @@ es:
password_min_length: La contraseña debe tener al menos 8 caracteres
password_mismatch: Las contraseñas no coinciden
password_updated: Contraseña actualizada. Ya puedes iniciar sesión.
+ accounts:
+ name_required: El nombre es obligatorio
+ profile_updated: Perfil actualizado.
+ password_current_incorrect: La contraseña actual no es correcta
+ password_too_short: La contraseña debe tener al menos 8 caracteres
+ password_mismatch: Las contraseñas no coinciden
+ password_updated: Contraseña actualizada.
replay:
download_unavailable: Descarga no disponible
not_available: Repetición no disponible
diff --git a/backend/config/locales/app.fr.yml b/backend/config/locales/app.fr.yml
index 836b2fe..bd8a4f0 100644
--- a/backend/config/locales/app.fr.yml
+++ b/backend/config/locales/app.fr.yml
@@ -606,6 +606,13 @@ fr:
password_min_length: Le mot de passe doit comporter au moins 8 caractères
password_mismatch: Les mots de passe ne correspondent pas
password_updated: Mot de passe mis à jour. Tu peux maintenant te connecter.
+ accounts:
+ name_required: Le nom est obligatoire
+ profile_updated: Profil mis à jour.
+ 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_mismatch: Les mots de passe ne correspondent pas
+ password_updated: Mot de passe mis à jour.
replay:
download_unavailable: Téléchargement non disponible
not_available: Replay non disponible
diff --git a/backend/config/locales/app.it.yml b/backend/config/locales/app.it.yml
index 91cfe9a..eee0e1d 100644
--- a/backend/config/locales/app.it.yml
+++ b/backend/config/locales/app.it.yml
@@ -606,6 +606,13 @@ it:
password_min_length: La password deve avere almeno 8 caratteri
password_mismatch: Le password non coincidono
password_updated: Password aggiornata. Ora puoi accedere.
+ accounts:
+ name_required: Il nome è obbligatorio
+ profile_updated: Profilo aggiornato.
+ password_current_incorrect: La password attuale non è corretta
+ password_too_short: La password deve avere almeno 8 caratteri
+ password_mismatch: Le password non coincidono
+ password_updated: Password aggiornata.
replay:
download_unavailable: Download non disponibile
not_available: Replay non disponibile
diff --git a/backend/config/locales/ui.de.yml b/backend/config/locales/ui.de.yml
index f591ade..9c3231a 100644
--- a/backend/config/locales/ui.de.yml
+++ b/backend/config/locales/ui.de.yml
@@ -16,6 +16,7 @@ de:
my_team: Mein Team
login: Anmelden
logout: Abmelden
+ account: Konto
signup: Team registrieren
footer:
tagline: Jedes Spiel, jedes Event, für alle, die nicht dabei sein können
@@ -118,6 +119,18 @@ de:
signup_link: registrieren Sie sich
mobile_app_label: "Mobile App:"
open_in_app: Einladung in der App öffnen
+ account:
+ meta_title: "Dein Konto — Match Live TV"
+ meta_description: Name und Passwort deines Match Live TV-Kontos verwalten.
+ title: Dein Konto
+ profile_heading: Profil
+ password_heading: Passwort ändern
+ name_label: Name
+ role_label: "Rolle: %{role}"
+ current_password_label: Aktuelles Passwort
+ new_password_label: "Neues Passwort (mind. 8 Zeichen)"
+ save_profile: Profil speichern
+ save_password: Passwort aktualisieren
common:
privacy: Datenschutz
cookies: Cookies
diff --git a/backend/config/locales/ui.en.yml b/backend/config/locales/ui.en.yml
index 0bb74b7..ea1e0ef 100644
--- a/backend/config/locales/ui.en.yml
+++ b/backend/config/locales/ui.en.yml
@@ -16,6 +16,7 @@ en:
my_team: My team
login: Log in
logout: Log out
+ account: Account
signup: Register a team
footer:
tagline: Every match, every event, for those who can't be there
@@ -118,6 +119,18 @@ en:
signup_link: sign up
mobile_app_label: "Mobile app:"
open_in_app: Open invitation in the app
+ account:
+ meta_title: "Your account — Match Live TV"
+ meta_description: Manage your Match Live TV account name and password.
+ title: Your account
+ profile_heading: Profile
+ password_heading: Change password
+ name_label: Name
+ role_label: "Role: %{role}"
+ current_password_label: Current password
+ new_password_label: "New password (min. 8 characters)"
+ save_profile: Save profile
+ save_password: Update password
common:
privacy: Privacy
cookies: Cookies
diff --git a/backend/config/locales/ui.es.yml b/backend/config/locales/ui.es.yml
index 7f210e8..d230d9a 100644
--- a/backend/config/locales/ui.es.yml
+++ b/backend/config/locales/ui.es.yml
@@ -16,6 +16,7 @@ es:
my_team: Mi equipo
login: Acceder
logout: Salir
+ account: Cuenta
signup: Registrar equipo
footer:
tagline: Cada partido, cada evento, para quien no puede estar
@@ -118,6 +119,18 @@ es:
signup_link: regístrate
mobile_app_label: "App móvil:"
open_in_app: Abrir invitación en la app
+ account:
+ meta_title: "Tu cuenta — Match Live TV"
+ meta_description: Gestiona el nombre y la contraseña de tu cuenta Match Live TV.
+ title: Tu cuenta
+ profile_heading: Perfil
+ password_heading: Cambiar contraseña
+ name_label: Nombre
+ role_label: "Rol: %{role}"
+ current_password_label: Contraseña actual
+ new_password_label: "Nueva contraseña (mín. 8 caracteres)"
+ save_profile: Guardar perfil
+ save_password: Actualizar contraseña
common:
privacy: Privacidad
cookies: Cookies
diff --git a/backend/config/locales/ui.fr.yml b/backend/config/locales/ui.fr.yml
index 4c690a3..4711125 100644
--- a/backend/config/locales/ui.fr.yml
+++ b/backend/config/locales/ui.fr.yml
@@ -16,6 +16,7 @@ fr:
my_team: Mon équipe
login: Connexion
logout: Déconnexion
+ account: Compte
signup: Inscrire une équipe
footer:
tagline: Chaque match, chaque événement, pour ceux qui ne peuvent pas être là
@@ -118,6 +119,18 @@ fr:
signup_link: inscrivez-vous
mobile_app_label: "Application mobile :"
open_in_app: Ouvrir l'invitation dans l'application
+ account:
+ meta_title: "Votre compte — Match Live TV"
+ meta_description: Gérez le nom et le mot de passe de votre compte Match Live TV.
+ title: Votre compte
+ profile_heading: Profil
+ password_heading: Changer le mot de passe
+ name_label: Nom
+ role_label: "Rôle : %{role}"
+ current_password_label: Mot de passe actuel
+ new_password_label: "Nouveau mot de passe (min. 8 caractères)"
+ save_profile: Enregistrer le profil
+ save_password: Mettre à jour le mot de passe
common:
privacy: Confidentialité
cookies: Cookies
diff --git a/backend/config/locales/ui.it.yml b/backend/config/locales/ui.it.yml
index deccb26..91e5a68 100644
--- a/backend/config/locales/ui.it.yml
+++ b/backend/config/locales/ui.it.yml
@@ -16,6 +16,7 @@ it:
my_team: La mia squadra
login: Accedi
logout: Esci
+ account: Account
signup: Registra squadra
footer:
tagline: Ogni partita, ogni evento, per chi non può esserci
@@ -118,6 +119,18 @@ it:
signup_link: registrati
mobile_app_label: "App mobile:"
open_in_app: Apri invito nell'app
+ account:
+ meta_title: "Il tuo account — Match Live TV"
+ meta_description: Gestisci nome e password del tuo account Match Live TV.
+ title: Il tuo account
+ profile_heading: Profilo
+ password_heading: Cambia password
+ name_label: Nome
+ role_label: "Ruolo: %{role}"
+ current_password_label: Password attuale
+ new_password_label: "Nuova password (min. 8 caratteri)"
+ save_profile: Salva profilo
+ save_password: Aggiorna password
common:
privacy: Privacy
cookies: Cookie
diff --git a/backend/config/routes.rb b/backend/config/routes.rb
index a2c7539..dd5d8cf 100644
--- a/backend/config/routes.rb
+++ b/backend/config/routes.rb
@@ -11,6 +11,10 @@ Rails.application.routes.draw do
post "auth/logout", to: "auth#logout"
post "auth/refresh", to: "auth#refresh"
get "auth/me", to: "auth#me"
+ post "auth/password/forgot", to: "auth#forgot_password"
+ get "account", to: "accounts#show"
+ patch "account", to: "accounts#update"
+ patch "account/password", to: "accounts#password"
get "sports", to: "sports#index"
get "invitations/:token", to: "invitations#show"
@@ -163,6 +167,9 @@ Rails.application.routes.draw do
post "password/forgot", to: "password_resets#create"
get "password/reset", to: "password_resets#edit", as: :password_reset
patch "password/reset", to: "password_resets#update"
+ get "account", to: "accounts#show", as: :account
+ patch "account", to: "accounts#update"
+ patch "account/password", to: "accounts#update_password", as: :account_password
get "clubs/new", to: "clubs#new", as: :new_club
post "clubs", to: "clubs#create"
get "clubs/:id", to: "clubs#show", as: :club
diff --git a/backend/spec/requests/account_spec.rb b/backend/spec/requests/account_spec.rb
new file mode 100644
index 0000000..a008fdb
--- /dev/null
+++ b/backend/spec/requests/account_spec.rb
@@ -0,0 +1,81 @@
+require "rails_helper"
+
+RSpec.describe "Account API", type: :request do
+ let!(:user) { User.create!(email: "account@example.com", name: "Account User", password: "password123", role: "coach") }
+ let(:auth_headers) do
+ post "/api/v1/auth/login", params: { email: user.email, password: "password123" }
+ token = JSON.parse(response.body).fetch("access_token")
+ { "Authorization" => "Bearer #{token}" }
+ end
+
+ describe "GET /api/v1/account" do
+ it "returns the current user profile" do
+ get "/api/v1/account", headers: auth_headers
+ expect(response).to have_http_status(:ok)
+ body = JSON.parse(response.body)
+ expect(body).to include("email" => user.email, "name" => "Account User", "role" => "coach")
+ end
+
+ it "requires authentication" do
+ get "/api/v1/account"
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
+
+ describe "PATCH /api/v1/account" do
+ it "updates the name" do
+ patch "/api/v1/account", params: { name: "Nuovo Nome" }, headers: auth_headers
+ expect(response).to have_http_status(:ok)
+ expect(JSON.parse(response.body)["name"]).to eq("Nuovo Nome")
+ expect(user.reload.name).to eq("Nuovo Nome")
+ end
+
+ it "rejects a blank name" do
+ patch "/api/v1/account", params: { name: " " }, headers: auth_headers
+ expect(response).to have_http_status(:unprocessable_entity)
+ end
+ end
+
+ describe "PATCH /api/v1/account/password" do
+ it "changes the password with the current password" do
+ patch "/api/v1/account/password",
+ params: {
+ current_password: "password123",
+ password: "newpass123",
+ password_confirmation: "newpass123"
+ },
+ headers: auth_headers
+ expect(response).to have_http_status(:ok)
+ expect(user.reload.authenticate("newpass123")).to be_truthy
+ end
+
+ it "rejects an incorrect current password" do
+ patch "/api/v1/account/password",
+ params: {
+ current_password: "wrong",
+ password: "newpass123",
+ password_confirmation: "newpass123"
+ },
+ headers: auth_headers
+ expect(response).to have_http_status(:unprocessable_entity)
+ expect(user.reload.authenticate("password123")).to be_truthy
+ end
+ end
+
+ describe "POST /api/v1/auth/password/forgot" do
+ it "always returns ok and sends mail when the user exists" do
+ expect {
+ post "/api/v1/auth/password/forgot", params: { email: user.email }
+ }.to change { ActionMailer::Base.deliveries.size }.by(1)
+ expect(response).to have_http_status(:ok)
+ expect(user.reload.password_reset_digest).to be_present
+ end
+
+ it "returns the same message for unknown emails" do
+ expect {
+ post "/api/v1/auth/password/forgot", params: { email: "nobody@example.com" }
+ }.not_to change { ActionMailer::Base.deliveries.size }
+ expect(response).to have_http_status(:ok)
+ end
+ end
+end
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
index d6635d3..aaa2863 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
@@ -13,6 +13,22 @@ data class LoginRequest(val email: String, val password: String)
data class RefreshRequest(@Json(name = "refresh_token") val refreshToken: String)
+data class ForgotPasswordRequest(val email: String)
+
+data class ForgotPasswordResponse(val message: String)
+
+data class UpdateAccountRequest(val name: String)
+
+data class ChangePasswordRequest(
+ @Json(name = "current_password") val currentPassword: String,
+ val password: String,
+ @Json(name = "password_confirmation") val passwordConfirmation: String,
+)
+
+data class MessageResponse(val message: String)
+
+data class ApiErrorResponse(val error: String? = null)
+
data class LoginResponse(
val user: UserDto,
@Json(name = "access_token") val accessToken: String,
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
index 8832028..b620567 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
@@ -23,6 +23,18 @@ interface MatchLiveApi {
@POST("auth/logout")
suspend fun logout()
+ @POST("auth/password/forgot")
+ suspend fun forgotPassword(@Body body: ForgotPasswordRequest): ForgotPasswordResponse
+
+ @GET("account")
+ suspend fun account(): UserDto
+
+ @PATCH("account")
+ suspend fun updateAccount(@Body body: UpdateAccountRequest): UserDto
+
+ @PATCH("account/password")
+ suspend fun changePassword(@Body body: ChangePasswordRequest): MessageResponse
+
@GET("sports")
suspend fun sports(): List