Aggiunge gestione account con cambio password su web e app.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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" %>
|
||||
|
||||
Reference in New Issue
Block a user