Add instructions to set password for OAuth users (#346)

This commit is contained in:
Riccardo Graziosi
2024-05-14 17:47:17 +02:00
committed by GitHub
parent 57ed8c338d
commit 5a162c6f4f
13 changed files with 116 additions and 9 deletions

View File

@@ -0,0 +1,19 @@
class PasswordsController < Devise::PasswordsController
# Needed to have Current.tenant available in Devise's controllers
prepend_before_action :load_tenant_data
# Needed for OAuth users (otherwise an already logged in user wouldn't be able to reset their password)
skip_before_action :require_no_authentication, :only => [:edit, :update]
def update
super
if resource.errors.empty?
resource.update!(has_set_password: true) unless resource.has_set_password?
# See: https://stackoverflow.com/a/22110985/1857435
sign_out(resource_name)
sign_in(resource_name, resource)
end
end
end

View File

@@ -17,6 +17,16 @@ class RegistrationsController < Devise::RegistrationsController
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
def send_set_password_instructions
user = User.find_by_email(params[:email])
if user.present?
user.send_reset_password_instructions
end
render json: { success: true } # always return true, even if user not found
end
private
def set_page_title_new

View File

@@ -42,6 +42,7 @@ class TenantsController < ApplicationController
full_name: params[:user][:full_name] || I18n.t('defaults.user_full_name'),
email: params[:user][:email],
password: is_o_auth_login ? Devise.friendly_token : params[:user][:password],
has_set_password: !is_o_auth_login,
role: "owner"
)

View File

@@ -125,7 +125,7 @@ class NewPost extends React.Component<Props, State> {
} catch (e) {
this.setState({
error: I18n.t('board.new_post.submit_error')
error: I18n.t('common.errors.unknown')
});
}
}

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import I18n from 'i18n-js';
import Button from '../common/Button';
import buildRequestHeaders from '../../helpers/buildRequestHeaders';
interface Props {
sendSetPasswordInstructionsEndpoint: string;
authenticityToken: string;
}
const SetPasswordDialog = ({ sendSetPasswordInstructionsEndpoint, authenticityToken }: Props) => {
return (
<div style={{margin: 8, padding: 16, backgroundColor: '#cce5ff', borderRadius: 4}}>
<p>
{ I18n.t('common.forms.auth.no_password_set') }
</p>
<Button
type='button'
onClick={async (event) => {
const res = await fetch(sendSetPasswordInstructionsEndpoint, {
method: 'GET',
headers: buildRequestHeaders(authenticityToken)
});
if (res.ok) {
alert(I18n.t('devise.passwords.send_instructions'));
} else {
alert(I18n.t('common.errors.unknown'));
}
}}>
{ I18n.t('common.forms.auth.set_password') }
</Button>
</div>
)
};
export default SetPasswordDialog;

View File

@@ -3,14 +3,16 @@ import * as React from 'react';
interface Props {
children: any;
onClick(e: React.FormEvent): void;
type?: 'button' | 'submit';
className?: string;
outline?: boolean;
disabled?: boolean;
}
const Button = ({ children, onClick, className = '', outline = false, disabled = false}: Props) => (
const Button = ({ children, onClick, type="submit", className = '', outline = false, disabled = false}: Props) => (
<button
onClick={onClick}
type={type}
className={`${className} btn${outline ? 'Outline' : ''}Primary`}
disabled={disabled}
>

View File

@@ -3,6 +3,20 @@
<%= render "devise/shared/error_messages", resource: resource %>
<% if not current_user.has_set_password? %>
<%=
react_component(
'UserProfile/SetPasswordDialog',
{
sendSetPasswordInstructionsEndpoint: send_set_password_instructions_path(email: current_user.email),
authenticityToken: form_authenticity_token
},
)
%>
<fieldset disabled="disabled">
<% end %>
<div class="form-group">
<%= f.label :full_name, t('common.forms.auth.full_name') %>
<%= f.text_field :full_name, autocomplete: "full-name", class: "form-control" %>
@@ -42,9 +56,11 @@
<%= f.password_field :password_confirmation, autocomplete: "new-password", class: "form-control" %>
</div>
<hr />
<div class="form-group">
<%= f.label :current_password, t('common.forms.auth.current_password') %>
<%= f.password_field :current_password, autocomplete: "current-password", class: "form-control" %>
<%= f.password_field :current_password, autocomplete: "current-password", required: true, class: "form-control" %>
<small id="currentPasswordHelp" class="form-text text-muted">
<%= t('common.forms.auth.current_password_required_help') %>
</small>
@@ -53,6 +69,10 @@
<div class="actions">
<%= f.submit t('common.forms.auth.update_profile'), class: "btnPrimary btn-block" %>
</div>
<% if not current_user.has_set_password? %>
</fieldset>
<% end %>
<% end %>
<br />

View File

@@ -44,6 +44,7 @@ class OAuthSignInUserWorkflow
email: email,
full_name: full_name,
password: Devise.friendly_token,
has_set_password: false,
status: 'active'
)
user.skip_confirmation

View File

@@ -1,5 +1,7 @@
en:
common:
errors:
unknown: 'An unknown error occurred, please try again'
validations:
required: '%{attribute} is required'
email: 'Invalid email'
@@ -18,6 +20,8 @@ en:
notifications_enabled: 'Notifications enabled'
notifications_enabled_help: "if disabled, you won't receive any notification"
waiting_confirmation: 'Currently waiting confirmation for %{email}'
no_password_set: 'You must set a password to update your profile'
set_password: 'Set password'
password_leave_blank_help: "leave blank if you don't want to change your password"
current_password_required_help: 'we need your current password to confirm your changes'
remember_me: 'Remember me'
@@ -92,7 +96,6 @@ en:
description: 'Description (optional)'
no_title: 'Title field is mandatory'
submit_success: 'Feedback published! You will be redirected soon...'
submit_error: 'An unknown error occurred, try again'
search_box:
title: 'Search'
filter_box:

View File

@@ -31,8 +31,13 @@ Rails.application.routes.draw do
devise_for :users, :controllers => {
:registrations => 'registrations',
:sessions => 'sessions'
:sessions => 'sessions',
:passwords => 'passwords'
}
devise_scope :user do
get '/users/send_set_password_instructions', to: 'registrations#send_set_password_instructions', as: :send_set_password_instructions
end
resources :tenants, only: [:show, :update]
resources :users, only: [:index, :update]

View File

@@ -0,0 +1,5 @@
class AddHasSetPasswordToUsers < ActiveRecord::Migration[6.1]
def change
add_column :users, :has_set_password, :boolean, default: true, null: false
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2024_04_27_140300) do
ActiveRecord::Schema.define(version: 2024_05_14_112836) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -198,6 +198,7 @@ ActiveRecord::Schema.define(version: 2024_04_27_140300) do
t.integer "status"
t.bigint "tenant_id", null: false
t.string "oauth_token"
t.boolean "has_set_password", default: true, null: false
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email", "tenant_id"], name: "index_users_on_email_and_tenant_id", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

View File

@@ -93,12 +93,11 @@ feature 'edit user profile settings', type: :system, js: true do
full_name = user.full_name
visit edit_user_registration_path
fill_in 'Full name', with: 'New fantastic full name'
fill_in 'Full name', with: user.full_name + ' New'
# do not fill current password textbox
click_button 'Update profile'
visit edit_user_registration_path
expect(page).to have_css('#error_explanation')
expect(page).to have_css('.field_with_errors')
expect(user.reload.full_name).to eq(full_name)
end