heartcombo / devise

Flexible authentication solution for Rails with Warden.
http://blog.plataformatec.com.br/tag/devise/
MIT License
23.83k stars 5.53k forks source link

Unprocessable Entity even after upgrading #5687

Closed simonsapiel closed 2 months ago

simonsapiel commented 2 months ago

I'm facing the same issue with turbo/devise compatibility as before the latest update. Upon errornous sign up I get the error

POST http://localhost:3000/users 422 (Unprocessable Entity) turbo.es2017-esm.js:688 

without the appropriate flash messages. I've followed the upgrade guide and added

  # ==> Hotwire/Turbo configuration
  # When using Devise with Hotwire/Turbo, the http status for error responses
  # and some redirects must match the following. The default in Devise for existing
  # apps is `200 OK` and `302 Found` respectively, but new apps are generated with
  # these new defaults that match Hotwire/Turbo behavior.
  # Note: These might become the new default in future versions of Devise.
  config.responder.error_status = :unprocessable_entity
  config.responder.redirect_status = :see_other
  config.navigational_formats = ['*/*', :html, :turbo_stream]

to my devise.rb config file and upgraded both of the gems

# Devise
gem 'devise', '~> 4.9', '>= 4.9.4'

# Responders
gem 'responders', '~> 3.1', '>= 3.1.1'

but the error persists. Is there a step missing from the upgrade guide? The biggest issue I'm facing here is that this doesn't show any flash messages when the user fails to sign up. The following illustrates what I mean.

https://github.com/heartcombo/devise/assets/61185966/ea84d2fa-0afd-4997-9423-d9c3b11a12ef

The flash messages will render properly on the sign in page, but not on the sign up page and both of them are pretty indentical with the registration page having the form as

<%= form_for(resource, as: resource_name, url: registration_path(resource_name), :html => {:class => 'mt-10 grid grid-cols-1 gap-y-8 gap-x-6 sm:grid-cols-2'}) do |f| %>
    <div class="">
        <label for="first_name" class="mb-3 block text-sm font-medium text-gray-700">Etunimi</label>
        <%= f.text_field :first_name, class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
    </div>
    <div class="">
        <label class="mb-3 block text-sm font-medium text-gray-700">Sukunimi</label>
        <%= f.text_field :last_name, class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
    </div>            
    <div class="col-span-full">
        <label for="email" class="mb-3 block text-sm font-medium text-gray-700">Sähköposti</label>
        <%= f.email_field :email, class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
    </div>
    <div class="">
        <label for="password" class="mb-3 block text-sm font-medium text-gray-700">Salasana</label>
        <%= f.password_field :password, class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
    </div>
    <div class="">
        <label class="mb-3 block text-sm font-medium text-gray-700">Salasanan vahvistus</label>
        <%= f.password_field :password_confirmation, class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
    </div>
    <div class="col-span-full">
        <%= f.button "Luo tili", class: "bg-blue-950 hover:bg-blue-1000 disabled:bg-blue-400 focus:ring-blue-500 inline-flex items-center justify-center border border-transparent rounded-md py-2 px-5 text-sm font-base text-white focus:ring-2 focus:ring-offset-2 focus:outline-none w-full" %>
    </div>
<% end %>

and the sign in page as

<%= form_for(resource, as: resource_name, html: { class: "space-y-5" }, url: session_path(resource_name)) do |f| %>
  <div class="relative">
      <div class="flex justify-between items-center"><label for="email" class="block font-medium text-sm text-gray-900">Sähköposti</label></div>
      <div class="mt-1">
          <%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm' %>
      </div>
  </div>
  <div class="relative" data-controller="password-visibility">
      <div class="flex justify-between items-center">
          <label for="password" class="block font-medium text-sm text-gray-900">Salasana</label>
      </div>
      <div class="mt-1 relative">
          <%= f.password_field :password,
              autocomplete: "current-password",
              class: 'block w-full appearance-none rounded-md border border-gray-200 bg-gray-50 px-3 py-2 text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:bg-white focus:outline-none focus:ring-blue-500 sm:text-sm',
              'data-password-visibility-target' => 'input' %>

          <button data-action="password-visibility#toggle" type="button" class="text-gray-500 absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer">
              <!-- Heroicon name: outline/eye -->
              <svg data-password-visibility-target="icon" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
              </svg>

              <!-- Heroicon name: outline/eye-off -->
              <svg data-password-visibility-target="icon" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
              </svg>
          </button>
      </div>
  </div>
  <div class="flex items-center justify-between">
      <div class="flex items-center">
          <%= f.check_box :remember_me, class: "h-4 w-4 text-blue-950 focus:ring-indigo-500 border-gray-300 rounded" %>
          <label for="remember_me" class="ml-2 block text-sm text-gray-900">Muista minut?</label>
      </div>
      <div class="text-sm"><a class="font-medium text-blue-950 hover:text-blue-1000" href="/users/password/new">Unohditko salasanasi?</a></div>
  </div>
  <div class="flex space-x-3">
      <%= f.button "Kirjaudu sisään", class: "bg-blue-950 hover:bg-blue-1000 disabled:bg-blue-400 focus:ring-blue-500 inline-flex items-center justify-center border border-transparent rounded-md py-2 px-5 text-sm font-base text-white focus:ring-2 focus:ring-offset-2 focus:outline-none w-full" %>
  </div>
  <% end %>

apologies from the language barrier.

carlosantoniodasilva commented 2 months ago

I don't think a failed signup returns a flash message for any errors, and a 422 Unprocessable Entity response there seems okay to me. The errors upon signup are validation errors and should be shown inline or however you're showing validation errors in general. Are you checking / displaying them in your form? (it doesn't look like from the bits you shared, but wanted to double check as they may be presented differently)

The flash message scenario described in the upgrade is more for sign in / unauthenticated redirects, where it uses the failure app to send yo back to the sign in page.

simonsapiel commented 2 months ago

@carlosantoniodasilva My user.rb model contains validations for both of the custom fields I have as follows:

validates :first_name, presence: { message: "Cannot be blank." }
validates :last_name, presence: { message: "Cannot be blank." }

but neither of them are being displayed. I've also tried to play with the create method to add the appropriate status to work with turbo, but this also doesn't seem to produce the flash messages.

class Users::RegistrationsController < Devise::RegistrationsController
  include Accessible
  skip_before_action :check_resource, except: %i[new create]

  def create
    build_resource(sign_up_params)

    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        set_flash_message! :notice, :signed_up
        sign_up(resource_name, resource)
        respond_with resource, location: after_sign_up_path_for(resource)
      else
        set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
        expire_data_after_sign_in!
        respond_with resource, location: after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      set_minimum_password_length
      debugger
      render :new, status: :unprocessable_entity
    end
  end
end

Also yes I'm rendering a partial

<%= render 'shared/notification' %>

in my application.html.erb that contains

<% if flash.any? %>
  <div class="flash fixed inset-0 flex items-end justify-center px-4 py-6 pointer-events-none sm:p-6 sm:items-start sm:justify-end z-50" data-flash-notifications='true' >
    <div class="w-full flex flex-col items-center space-y-4 sm:items-end">
    <% flash.each do |msg_type, msg| %>
      <% if msg.is_a?(String) %>
        <div x-data="{flashVisible: false, flashType: '<%= msg_type %>'}" x-cloak x-show='flashVisible' x-init="() => {flashVisible=true; setTimeout(() => {flashVisible=false}, 2500)}" class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden"
            x-transition:enter="transition ease-out duration-300"
            x-transition:enter-start="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
            x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0"
            x-transition:leave="transition ease-out duration-300"
            x-transition:leave-start="transform opacity-100"
            x-transition:leave-end="transform opacity-0">
          <div class="rounded-lg shadow-xs overflow-hidden">
            <div class="p-4">
              <div class="flex items-start">
                <div class="flex-shrink-0">
                  <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"
                      x-bind:class="{'text-green-400': flashType == 'success', 'text-red-600': flashType == 'error', 'text-red-600': flashType == 'alert', 'text-green-600': flashType == 'notice'}">
                    <path x-show="flashType == 'success'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
                    <path x-show="flashType == 'alert'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                    <path x-show="flashType == 'error'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                    <path x-show="flashType == 'notice'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                  </svg>
                </div>
                <div class="ml-3 w-0 flex-1 pt-0.5">
                  <p class="text-sm leading-5 font-medium text-gray-900">
                    <%= msg %> 
                  </p>
                </div>
                <div class="ml-4 flex-shrink-0 flex">
                  <button x-on:click="flashVisible = false" class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150">
                    <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                      <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
                    </svg>
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      <% else %>
        <% msg.each do |m| %>
          <div x-data="{flashVisible: false, flashType: '<%= msg_type %>'}" x-cloak x-show='flashVisible' x-init="() => {flashVisible=true; setTimeout(() => {flashVisible=false}, 2500)}" class="max-w-sm w-full bg-white shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden"
              x-transition:enter="transition ease-out duration-300"
              x-transition:enter-start="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
              x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0"
              x-transition:leave="transition ease-out duration-300"
              x-transition:leave-start="transform opacity-100"
              x-transition:leave-end="transform opacity-0">
            <div class="rounded-lg shadow-xs overflow-hidden">
              <div class="p-4">
                <div class="flex items-start">
                  <div class="flex-shrink-0">
                    <svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"
                        x-bind:class="{'text-green-400': flashType == 'success', 'text-red-600': flashType == 'error', 'text-red-600': flashType == 'alert', 'text-green-600': flashType == 'notice'}">
                      <path x-show="flashType == 'success'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
                      <path x-show="flashType == 'alert'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                      <path x-show="flashType == 'error'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
                      <path x-show="flashType == 'notice'" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
                    </svg>
                  </div>
                  <div class="ml-3 w-0 flex-1 pt-0.5">
                    <p class="text-sm leading-5 font-medium text-gray-900">
                      <%= m %> 
                    </p>
                  </div>
                  <div class="ml-4 flex-shrink-0 flex">
                    <button x-on:click="flashVisible = false" class="inline-flex text-gray-400 focus:outline-none focus:text-gray-500 transition ease-in-out duration-150">
                      <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                        <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
                      </svg>
                    </button>
                  </div>
                </div>
              </div>
            </div>
          </div>
        <% end %>
      <% end %>
    <% end %>  
    </div>
  </div>
<% end %>

Apologies for the cluttered html, but it's working for all my other flash messages appropriately.

carlosantoniodasilva commented 2 months ago

@simonsapiel thanks for sharing these extra bits... like I mentioned before, there's no flash messages supposed to be shown for signup errors, they're just model validation errors. (which is different than sign in)

Can you try displaying all errors on your form object (i.e. the resource) with something like this?

  <%= render "devise/shared/error_messages", resource: resource %>
simonsapiel commented 2 months ago

@carlosantoniodasilva Adding this the errors show up as

Screenshot from 2024-05-09 16-44-47

The first sentence is just the usual error message in my current locale. 4 fields as I left all the 4 fields blank. So the issue I'm having seems not to be related to the new upgrade, but rather than the sign up form not supposed to be showing any flash messages? Is there documentation somewhere on how I could display these errors as flash messages so that I could render them with the partial like I have on the sign in page?

simonsapiel commented 2 months ago

Alright, I managed to get the wanted functionality by

  def create
    super do |user|
      unless user.persisted?
        clean_up_passwords user
        set_minimum_password_length
        redirect_back(fallback_location: root_path, alert: user.errors.full_messages)
        return
      end
    end
  end

not sure if this is a good way to go though.

carlosantoniodasilva commented 2 months ago

the sign up form not supposed to be showing any flash messages?

Exactly... The sign in page is the one that's actually "different", in that it displays a flash message for invalid attempts. The sign up form is much like any other form, in which the attributes will have errors from the validations added to them, and you can display them however you'd like. (like any other forms on your app would, for instance -- inline, as a group of messages at the top, etc.)

If you want to turn those into a flash message, I suppose you could override the create action and add a flash, with something like: (untested)

def create
  super do |resource|
    unless resource.persisted?
      flash.now[:alert] = resource.errors.full_messages
    end
  end
end
carlosantoniodasilva commented 2 months ago

@simonsapiel your solution should work as well for the flash, but you might be able to skip the redirecting as that would probably lose the user context (i.e. input values that the user typed, etc.).