Rafforza i requisiti password con regole di complessità di mercato.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,13 +9,18 @@ module Admin
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password].blank? || params[:password].length < 8
|
||||
flash.now[:alert] = t("admin.flash.password_too_short")
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("admin.flash.password_mismatch")
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("admin.flash.password_mismatch")
|
||||
if (code = PasswordComplexity.violation(params[:password]))
|
||||
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
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ module Api
|
||||
case code
|
||||
when :current_incorrect then "Current password is incorrect"
|
||||
when :too_short then "Password must be at least 8 characters"
|
||||
when :too_long then "Password cannot exceed 72 characters"
|
||||
when :too_weak
|
||||
"Password must include at least 3 of: lowercase, uppercase, number, symbol"
|
||||
when :mismatch then "Passwords do not match"
|
||||
else "Unable to update password"
|
||||
end
|
||||
|
||||
@@ -28,14 +28,14 @@ module Public
|
||||
return
|
||||
end
|
||||
|
||||
if params[:password].blank? || params[:password].length < 8
|
||||
flash.now[:alert] = t("flash.password_resets.password_min_length")
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
||||
@token = params[:token]
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if params[:password] != params[:password_confirmation]
|
||||
flash.now[:alert] = t("flash.password_resets.password_mismatch")
|
||||
if (code = PasswordComplexity.violation(params[:password]))
|
||||
flash.now[:alert] = t("flash.password_resets.password_#{code == :blank ? :too_short : code}")
|
||||
@token = params[:token]
|
||||
return render :edit, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class AdminAccount < ApplicationRecord
|
||||
include PasswordComplexity
|
||||
|
||||
has_secure_password
|
||||
|
||||
validates :username, presence: true, uniqueness: true
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
include PasswordComplexity
|
||||
|
||||
ROLES = %w[admin coach parent volunteer].freeze
|
||||
|
||||
has_secure_password
|
||||
|
||||
@@ -23,15 +23,19 @@ module Users
|
||||
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)
|
||||
if (code = PasswordComplexity.violation(@password))
|
||||
return Result.new(ok?: false, error: code == :blank ? :too_short : code)
|
||||
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!
|
||||
Result.new(ok?: true)
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
<div class="card">
|
||||
<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 %>
|
||||
<%= render "shared/input_toggle",
|
||||
name: :current_password,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<section class="auth-page">
|
||||
<h1><%= t("auth.password_reset.title") %></h1>
|
||||
<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 %>
|
||||
<%= hidden_field_tag :token, @token %>
|
||||
<%= render "shared/input_toggle",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
required: true,
|
||||
minlength: 8,
|
||||
autocomplete: "new-password" %>
|
||||
<p class="muted" style="font-size:0.9rem;margin-top:-0.5rem"><%= t("password_policy.hint") %></p>
|
||||
<%= render "shared/input_toggle",
|
||||
name: "user[password_confirmation]",
|
||||
id: "user_password_confirmation",
|
||||
|
||||
Reference in New Issue
Block a user