forgot password implemented + refactoring
did
committed Nov 10, 2016
commit 6ce6273ecf3e62f8ac0120752fda4a425ac01ace
Showing 11
changed files with
332 additions
and 45 deletions
locomotive/steam/middlewares/auth.rb b/lib/locomotive/steam/middlewares/auth.rb
+108
-43
| @@ | @@ -7,43 +7,61 @@ module Locomotive::Steam |
| # - reset password | |
| # - sign out | |
| # | |
| + | # It is also in charge to load the current authenticated resource |
| + | # from the session and put it in the liquid context. |
| + | # |
| class Auth < ThreadSafe | |
| include Helpers | |
| - | ACTIONS = %w(sign_in sign_out send_password_reset reset_password) |
| - | |
| def _call | |
| load_authenticated_entry | |
| - | case params[:auth_action] |
| - | when 'sign_in' then sign_in |
| - | when 'sign_out' then sign_out |
| - | end |
| + | auth_options = AuthOptions.new(params) |
| - | # if ACTIONS.include?(params[:auth]) |
| - | # sign_in if params[:auth] == 'sign_in' |
| - | # end |
| + | return unless auth_options.valid? |
| + | |
| + | send(:"#{auth_options.action}", auth_options) |
| end | |
| private | |
| - | def load_authenticated_entry |
| - | if (entry_id = request.session[:authenticated_entry_id]) && |
| - | (entry_type = request.session[:authenticated_entry_type]) |
| + | def sign_in(options) |
| + | return if authenticated? |
| - | env['authenticated_entry'] = find_entry(entry_type, entry_id) |
| + | status, entry = services.auth.sign_in(options) |
| - | liquid_assigns["current_#{entry_type.singularize}"] = decorate_entry(env['authenticated_entry']) |
| + | if status == :signed_in |
| + | store_authenticated(entry) |
| + | redirect_to options.callback || mounted_on |
| end | |
| + | |
| + | append_message(status) |
| end | |
| - | def find_entry(type, id) |
| - | begin |
| - | repositories.content_entry.with(type).find(id) |
| - | rescue Exception => e |
| - | log "Unable to find the authenticated entry: #{type}, #{id}" |
| - | nil |
| + | def sign_out(options) |
| + | return unless authenticated? |
| + | |
| + | store_authenticated(nil) |
| + | |
| + | append_message(:signed_out) |
| + | end |
| + | |
| + | def forgot_password(options) |
| + | return if authenticated? |
| + | |
| + | status = services.auth.forgot_password(options, default_liquid_context) |
| + | |
| + | append_message(status) |
| + | end |
| + | |
| + | def load_authenticated_entry |
| + | entry_type = request.session[:authenticated_entry_type] |
| + | entry_id = request.session[:authenticated_entry_id] |
| + | |
| + | if entry = services.auth.find_authenticated_resource(entry_type, entry_id) |
| + | env['authenticated_entry'] = entry |
| + | liquid_assigns["current_#{entry_type.singularize}"] = entry |
| end | |
| end | |
| @@ | @@ -51,39 +69,86 @@ module Locomotive::Steam |
| !!env['authenticated_entry'] | |
| end | |
| - | def sign_out |
| - | return unless authenticated? |
| + | def store_authenticated(entry) |
| + | type = entry ? entry.content_type.slug : request.session[:authenticated_entry_type] |
| - | type = request.session[:authenticated_entry_type] |
| + | request.session[:authenticated_entry_type] = type |
| + | request.session[:authenticated_entry_id] = entry.try(:_id) |
| - | request.session[:authenticated_entry_id] = nil |
| - | request.session[:authenticated_entry_type] = nil |
| + | liquid_assigns["current_#{type.singularize}"] = entry |
| + | end |
| - | liquid_assigns["current_#{type.singularize}"] = nil |
| - | liquid_assigns['auth_signed_out'] = 'auth_signed_out' |
| + | def append_message(message) |
| + | message ||= 'error' |
| + | liquid_assigns["auth_#{message}"] = "auth_#{message}" |
| end | |
| - | def sign_in |
| - | unless authenticated? |
| - | if type = repositories.content_type.by_slug(params[:auth_content_type]) |
| - | id_field = params[:auth_id_field] || :email |
| + | class AuthOptions |
| - | entry = repositories.content_entry.with(type).all(id_field => params[:auth_id]).first |
| + | ACTIONS = %w(sign_in sign_out forgot_password reset_password) |
| - | if entry && entry.password == params[:auth_password] |
| - | request.session[:authenticated_entry_id] = entry._id |
| - | request.session[:authenticated_entry_type] = type.slug |
| + | attr_reader :params |
| - | liquid_assigns["current_#{type.slug.singularize}"] = decorate_entry(entry) |
| + | def initialize(params) |
| + | @params = params |
| + | end |
| - | redirect_to params[:auth_callback] || mounted_on |
| - | else |
| - | liquid_assigns['auth_wrong_credentials'] = 'auth_wrong_credentials' |
| - | end |
| - | else |
| - | log "'#{params[:auth_content_type]}' is not a content type for authentication." |
| - | end |
| + | def valid? |
| + | ACTIONS.include?(action) |
| end | |
| + | |
| + | def action |
| + | params[:auth_action] |
| + | end |
| + | |
| + | def type |
| + | params[:auth_content_type] |
| + | end |
| + | |
| + | def id_field |
| + | params[:auth_id_field] || :email |
| + | end |
| + | |
| + | def password_field |
| + | params[:auth_password_field].try(:to_sym) || :password |
| + | end |
| + | |
| + | def id |
| + | params[:auth_id] |
| + | end |
| + | |
| + | def password |
| + | params[:auth_password] |
| + | end |
| + | |
| + | def callback |
| + | params[:auth_callback] |
| + | end |
| + | |
| + | def reset_password_url |
| + | params[:auth_reset_password_url] |
| + | end |
| + | |
| + | def from |
| + | params[:auth_email_from] || 'support@locomotivecms.com' |
| + | end |
| + | |
| + | def subject |
| + | params[:auth_email_subject] || 'Instructions for changing your password' |
| + | end |
| + | |
| + | def email_handle |
| + | params[:auth_email_handle] |
| + | end |
| + | |
| + | def smtp |
| + | { |
| + | address: params[:auth_email_smtp_address], |
| + | user_name: params[:auth_email_smtp_user_name], |
| + | password: params[:auth_email_smtp_password] |
| + | } |
| + | end |
| + | |
| end | |
| end | |
locomotive/steam/middlewares/thread_safe.rb b/lib/locomotive/steam/middlewares/thread_safe.rb
+10
-0
| @@ | @@ -74,6 +74,16 @@ module Locomotive::Steam::Middlewares |
| Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale, default_locale) | |
| end | |
| + | def default_liquid_context |
| + | ::Liquid::Context.new({ 'site' => site.to_liquid }, {}, { |
| + | request: request, |
| + | locale: locale, |
| + | site: site, |
| + | services: services, |
| + | repositories: services.repositories |
| + | }, true) |
| + | end |
| + | |
| end | |
| end | |
locomotive/steam/services.rb b/lib/locomotive/steam/services.rb
+5
-1
| @@ | @@ -79,7 +79,7 @@ module Locomotive |
| end | |
| register :url_builder do | |
| - | Steam::UrlBuilderService.new(current_site, locale, request) |
| + | Steam::UrlBuilderService.new(current_site, locale, request, page_finder) |
| end | |
| register :theme_asset_url do | |
| @@ | @@ -118,6 +118,10 @@ module Locomotive |
| Steam::EmailService.new(page_finder, liquid_parser, asset_host, configuration.mode == :test) | |
| end | |
| + | register :auth do |
| + | Steam::AuthService.new(content_entry, email) |
| + | end |
| + | |
| register :cache do | |
| Steam::NoCacheService.new | |
| end | |
locomotive/steam/services/auth_service.rb b/lib/locomotive/steam/services/auth_service.rb
+78
-0
| @@ | @@ -0,0 +1,78 @@ |
| + | module Locomotive |
| + | module Steam |
| + | |
| + | class AuthService |
| + | |
| + | attr_accessor_initialize :entries, :email_service |
| + | |
| + | def find_authenticated_resource(type, id) |
| + | entries.find(type, id) |
| + | end |
| + | |
| + | def sign_in(options) |
| + | entry = entries.all(options.type, options.id_field => options.id).first |
| + | |
| + | if entry |
| + | hashed_password = entry[:"#{options.password_field}_hash"] |
| + | password = ::BCrypt::Engine.hash_secret(options.password, entry.send(options.password_field).try(:salt)) |
| + | same_password = secure_compare(password, hashed_password) |
| + | |
| + | return [:signed_in, entry] if same_password |
| + | end |
| + | |
| + | :wrong_credentials |
| + | end |
| + | |
| + | # options is an instance of the AuthOptions class |
| + | def forgot_password(options, context) |
| + | entry = entries.all(options.type, options.id_field => options.id).first |
| + | |
| + | if entry.nil? |
| + | :wrong_email |
| + | else |
| + | entries.update_decorated_entry(entry, { |
| + | '_auth_reset_token' => SecureRandom.hex, |
| + | '_auth_reset_sent_at' => Time.zone.now |
| + | }) |
| + | |
| + | context['reset_password_url'] = options.reset_password_url + '?token=' + entry['_auth_reset_token'] |
| + | context[options.type.singularize] = entry |
| + | |
| + | send_reset_password_instructions(options, context) |
| + | |
| + | :reset_password_instructions_sent |
| + | end |
| + | end |
| + | |
| + | private |
| + | |
| + | def send_reset_password_instructions(options, context) |
| + | email_options = { from: options.from, to: options.id, subject: options.subject, smtp: options.smtp } |
| + | |
| + | if options.email_handle |
| + | email_options[:page_handle] = options.email_handle |
| + | else |
| + | email_options[:body] = <<-EMAIL |
| + | Hi, |
| + | To reset your password please follow the link below: #{context['reset_password_url']}. |
| + | Thanks! |
| + | |
| + | end |
| + | |
| + | email_service.send_email(email_options, context) |
| + | end |
| + | |
| + | # https://github.com/plataformatec/devise/blob/88724e10adaf9ffd1d8dbfbaadda2b9d40de756a/lib/devise.rb#L485 |
| + | def secure_compare(a, b) |
| + | return false if a.blank? || b.blank? || a.bytesize != b.bytesize |
| + | l = a.unpack "C#{a.bytesize}" |
| + | |
| + | res = 0 |
| + | b.each_byte { |byte| res |= byte ^ l.shift } |
| + | res == 0 |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
locomotive/steam/services/content_entry_service.rb b/lib/locomotive/steam/services/content_entry_service.rb
+16
-0
| @@ | @@ -56,6 +56,22 @@ module Locomotive |
| end | |
| end | |
| + | def update_decorated_entry(decorated_entry, attributes) |
| + | with_repository(decorated_entry.content_type) do |_repository| |
| + | entry = decorated_entry.__getobj__ |
| + | |
| + | puts clean_attributes(attributes).inspect |
| + | |
| + | entry.change(clean_attributes(attributes)) |
| + | |
| + | _repository.update(entry) |
| + | |
| + | logEntryOperation(decorated_entry.content_type.slug, decorated_entry) |
| + | |
| + | decorated_entry |
| + | end |
| + | end |
| + | |
| def delete(type_slug, id_or_slug) | |
| with_repository(type_slug) do |_repository| | |
| entry = _repository.by_slug(id_or_slug) || _repository.find(id_or_slug) | |
locomotive/steam/services/url_builder_service.rb b/lib/locomotive/steam/services/url_builder_service.rb
+7
-1
| @@ | @@ -3,9 +3,15 @@ module Locomotive |
| class UrlBuilderService | |
| - | attr_accessor_initialize :site, :current_locale, :request |
| + | attr_accessor_initialize :site, :current_locale, :request, :page_finder |
| + | |
| + | def absolute_url_for(page, locale = nil) |
| + | request.base_url + url_for(page, locale) |
| + | end |
| def url_for(page, locale = nil) | |
| + | page = page_finder.by_handle(page) if page.is_a?(String) |
| + | |
| prefix(_url_for(page, locale)) | |
| end | |
spec/fixtures/default/app/views/pages/account/forgot_password.liquid
+39
-0
| @@ | @@ -0,0 +1,39 @@ |
| + | --- |
| + | title: Forgot password |
| + | published: true |
| + | listed: false |
| + | handle: forgot_password |
| + | --- |
| + | {% extends 'index' %} |
| + | |
| + | {% block content %} |
| + | |
| + | <h1>Forgot your password</h1> |
| + | |
| + | {% if current_account %} |
| + | <div class="alert alert-warning"> |
| + | You're already authenticated! |
| + | </div> |
| + | {% else %} |
| + | {% if auth_reset_password_instructions_sent %} |
| + | {{ auth_reset_password_instructions_sent | translate }} |
| + | {% else %} |
| + | <form action="{% path_to 'forgot_password' %}" method="POST"> |
| + | <input type="hidden" name="auth_action" value="forgot_password" /> |
| + | <input type="hidden" name="auth_content_type" value="accounts" /> |
| + | <input type="hidden" name="auth_id_field" value="email" /> |
| + | <input type="hidden" name="auth_callback" value="{% path_to sign_in %}" /> |
| + | |
| + | {% if auth_wrong_email %} |
| + | {{ auth_wrong_email | translate }} |
| + | {% endif %} |
| + | |
| + | <label for="auth-email">Your E-mail</label> |
| + | <input type="email" id="auth-email" placeholder="Email" name="auth_id" value="{{ params.auth_id }}"> |
| + | |
| + | <button type="submit" class="btn btn-default">Submit</button> |
| + | </form> |
| + | {% endif %} |
| + | {% endif %} |
| + | |
| + | {% endblock %} |
spec/fixtures/default/app/views/pages/account/reset_password.liquid
+1
-0
| @@ | @@ -0,0 +1 @@ |
| + | reset_password.liquid |
spec/fixtures/default/app/views/pages/emails/reset_password.liquid
+15
-0
| @@ | @@ -0,0 +1,15 @@ |
| + | --- |
| + | title: Reset password instructions |
| + | listed: false |
| + | published: true |
| + | handle: reset_password_instructions |
| + | --- |
| + | Hello {{ account.name }}, |
| + | |
| + | To reset your password please follow the link below: |
| + | |
| + | {{ reset_password_url }} |
| + | |
| + | If you need help or have any questions, please visit https://www.locomotivecms.com |
| + | Thanks! |
| + | Locomotive |
spec/fixtures/default/config/translations.yml
+8
-0
| @@ | @@ -9,3 +9,11 @@ auth_wrong_credentials: |
| auth_signed_out: | |
| en: You've been signed out | |
| fr: Vous avez été déconnecté(e) | |
| + | |
| + | auth_wrong_email: |
| + | en: Your email is unknown |
| + | fr: Votre email est inconnu |
| + | |
| + | auth_reset_password_instructions_sent: |
| + | en: "The instructions for changing your password have been emailed to you" |
| + | fr: "Les instructions pour modifier votre mot de passe viennent de vous être envoyées" |
spec/integration/server/auth_spec.rb
+45
-0
| @@ | @@ -90,6 +90,51 @@ describe 'Authentication' do |
| end | |
| + | describe 'forgot password action' do |
| + | |
| + | let(:email) { '' } |
| + | |
| + | let(:params) { { |
| + | auth_action: 'forgot_password', |
| + | auth_content_type: 'accounts', |
| + | auth_id_field: 'email', |
| + | auth_id: email, |
| + | auth_reset_password_url: 'http://acme.com/account/reset-password', |
| + | auth_callback: '/account/sign-in', |
| + | auth_email_from: 'support@acme.com', |
| + | auth_email_handle: 'reset_password_instructions', |
| + | auth_email_smtp_address: 'smtp.nowhere.net', |
| + | auth_email_smtp_user_name: 'jane', |
| + | auth_email_smtp_password: 'easyone' |
| + | } } |
| + | |
| + | it 'renders the forgot password page with an error message' do |
| + | forgot_password |
| + | expect(last_response.status).to eq 200 |
| + | expect(last_response.body).to include 'Forgot your password' |
| + | expect(last_response.body).to include 'Your email is unknown' |
| + | end |
| + | |
| + | context 'with an known email' do |
| + | |
| + | let(:email) { 'john@doe.net' } |
| + | |
| + | it 'sends an email to the account' do |
| + | forgot_password |
| + | expect(last_response.status).to eq 200 |
| + | expect(last_response.body).to include "The instructions for changing your password have been emailed to you" |
| + | end |
| + | |
| + | end |
| + | |
| + | def forgot_password(follow_redirect = false) |
| + | post '/account/forgot-password', params |
| + | follow_redirect! if follow_redirect |
| + | last_response |
| + | end |
| + | |
| + | end |
| + | |
| end | |