Aggiunge gestione account con cambio password su web e app.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-08 10:38:57 +02:00
co-authored by Cursor
parent b8694a6b7b
commit 83a804a709
46 changed files with 1591 additions and 34 deletions
@@ -0,0 +1,57 @@
module Api
module V1
class AccountsController < ApplicationController
def show
render json: user_json(current_user)
end
def update
name = params[:name].to_s.strip
if name.blank?
return render json: { error: "Name is required" }, status: :unprocessable_entity
end
if current_user.update(name: name)
render json: user_json(current_user)
else
render json: { error: current_user.errors.full_messages.join(", ") }, status: :unprocessable_entity
end
end
def password
result = Users::ChangePassword.call(
user: current_user,
current_password: params[:current_password],
password: params[:password],
password_confirmation: params[:password_confirmation]
)
unless result.ok?
return render json: { error: password_error_message(result.error) }, status: :unprocessable_entity
end
render json: { message: "Password updated" }
end
private
def user_json(user)
{
id: user.id,
email: user.email,
name: user.name,
role: user.role
}
end
def password_error_message(code)
case code
when :current_incorrect then "Current password is incorrect"
when :too_short then "Password must be at least 8 characters"
when :mismatch then "Passwords do not match"
else "Unable to update password"
end
end
end
end
end
@@ -1,7 +1,7 @@
module Api
module V1
class AuthController < ApplicationController
skip_before_action :authenticate_request!, only: %i[login refresh register]
skip_before_action :authenticate_request!, only: %i[login refresh register forgot_password]
def register
user = User.new(
@@ -42,6 +42,13 @@ module Api
render json: user_json(current_user)
end
def forgot_password
Users::RequestPasswordReset.call(email: params[:email])
render json: {
message: "If the email is registered, you will receive a password reset link shortly."
}
end
private
def token_response(user)
@@ -0,0 +1,39 @@
module Public
class AccountsController < WebBaseController
before_action :require_login!
def show
end
def update
name = params[:name].to_s.strip
if name.blank?
flash.now[:alert] = t("flash.accounts.name_required")
return render :show, status: :unprocessable_entity
end
if current_user.update(name: name)
redirect_to public_account_path, notice: t("flash.accounts.profile_updated")
else
flash.now[:alert] = current_user.errors.full_messages.to_sentence
render :show, status: :unprocessable_entity
end
end
def update_password
result = Users::ChangePassword.call(
user: current_user,
current_password: params[:current_password],
password: params[:password],
password_confirmation: params[:password_confirmation]
)
unless result.ok?
flash.now[:alert] = t("flash.accounts.password_#{result.error}")
return render :show, status: :unprocessable_entity
end
redirect_to public_account_path, notice: t("flash.accounts.password_updated")
end
end
end
@@ -4,11 +4,7 @@ module Public
end
def create
user = User.find_by(email: params[:email]&.downcase&.strip)
if user
token = user.generate_password_reset!
UserMailer.password_reset(user, token).deliver_now
end
Users::RequestPasswordReset.call(email: params[:email])
redirect_to public_login_path,
notice: t("flash.password_resets.email_sent")
@@ -0,0 +1,39 @@
module Users
class ChangePassword
Result = Struct.new(:ok?, :error, keyword_init: true)
def self.call(user:, current_password:, password:, password_confirmation:)
new(
user: user,
current_password: current_password,
password: password,
password_confirmation: password_confirmation
).call
end
def initialize(user:, current_password:, password:, password_confirmation:)
@user = user
@current_password = current_password.to_s
@password = password.to_s
@password_confirmation = password_confirmation.to_s
end
def call
unless @user.authenticate(@current_password)
return Result.new(ok?: false, error: :current_incorrect)
end
if @password.blank? || @password.length < 8
return Result.new(ok?: false, error: :too_short)
end
if @password != @password_confirmation
return Result.new(ok?: false, error: :mismatch)
end
@user.update!(password: @password)
@user.clear_password_reset!
Result.new(ok?: true)
end
end
end
@@ -0,0 +1,19 @@
module Users
class RequestPasswordReset
def self.call(email:)
new(email: email).call
end
def initialize(email:)
@email = email.to_s.downcase.strip
end
def call
user = User.find_by(email: @email)
return if user.nil?
token = user.generate_password_reset!
UserMailer.password_reset(user, token).deliver_now
end
end
end
@@ -41,6 +41,7 @@
<% elsif current_user.manageable_teams.any? %>
· <%= link_to t("nav.my_team"), public_team_details_path(current_user.manageable_teams.first) %>
<% end %>
· <%= link_to t("nav.account"), public_account_path %>
· <%= button_to t("nav.logout"), public_logout_path, method: :delete, form: { style: "display:inline" }, class: "btn btn-secondary", style: "padding:6px 12px;font-size:0.85rem" %>
<% else %>
· <%= link_to t("nav.login"), public_login_path %>
@@ -0,0 +1,55 @@
<% content_for :title, t("auth.account.meta_title") %>
<% content_for :meta_description, t("auth.account.meta_description") %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1><%= t("auth.account.title") %></h1>
<div class="card" style="margin-bottom:1.5rem">
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.profile_heading") %></h2>
<%= form_with url: public_account_path, method: :patch, local: true do %>
<p>
<label for="account_email"><%= t("auth.email") %></label><br>
<input type="email" id="account_email" value="<%= current_user.email %>" readonly
class="input" autocomplete="username"
style="width:100%;opacity:0.75;cursor:not-allowed">
</p>
<p class="muted" style="margin-top:-0.5rem;font-size:0.9rem">
<%= t("auth.account.role_label", role: current_user.role) %>
</p>
<%= 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 %>
</div>
<div class="card">
<h2 style="font-size:1.1rem;margin-top:0"><%= t("auth.account.password_heading") %></h2>
<%= 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 %>
</div>
</section>
@@ -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" %>