sign in/sign out implemented (but need some refactoring)

did committed Nov 09, 2016
commit 2bf17b3efffd2f06a029fb8de93c66acbbad3a79
Showing 20 changed files with 426 additions and 11 deletions
Gemfile.lock +8 -3
@@ @@ -1,9 +1,10 @@
PATH
remote: .
specs:
- locomotivecms_steam (1.2.0.rc3)
+ locomotivecms_steam (1.3.0)
RedCloth (~> 4.3.2)
autoprefixer-rails (~> 6.3.3.1)
+ bcrypt (~> 3.1.11)
chronic (~> 0.10.2)
coffee-script (~> 2.4.1)
compass (~> 1.0.3)
@@ @@ -25,6 +26,7 @@ PATH
sanitize (~> 4.0.1)
sass (~> 3.4.21)
sprockets (~> 3.5.2)
+ warden (~> 1.2.6)
GEM
remote: https://rubygems.org/
@@ @@ -40,6 +42,7 @@ GEM
attr_extras (4.4.0)
autoprefixer-rails (6.3.3.1)
execjs
+ bcrypt (3.1.11)
bson (4.1.1)
byebug (8.2.5)
chronic (0.10.2)
@@ @@ -121,7 +124,7 @@ GEM
multi_xml (0.5.5)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
- nokogumbo (1.4.9)
+ nokogumbo (1.4.10)
nokogiri
origin (2.2.0)
pony (1.11)
@@ @@ -145,7 +148,7 @@ GEM
rack_csrf (2.5.0)
rack (>= 1.1.0)
rake (10.4.2)
- rb-fsevent (0.9.7)
+ rb-fsevent (0.9.8)
rb-inotify (0.9.7)
ffi (>= 0.5.0)
rspec (3.4.0)
@@ @@ -186,6 +189,8 @@ GEM
tins (1.12.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
+ warden (1.2.6)
+ rack (>= 1.0)
yui-compressor (0.12.0)
PLATFORMS
locomotive/steam/adapters/filesystem/yaml_loaders/content_entry.rb b/lib/locomotive/steam/adapters/filesystem/yaml_loaders/content_entry.rb +8 -0
@@ @@ -23,6 +23,7 @@ module Locomotive
modify_for_selects(_attributes)
modify_for_associations(_attributes)
modify_for_files(_attributes)
+ modify_for_passwords(_attributes)
list << _attributes
end
@@ @@ -56,6 +57,13 @@ module Locomotive
end
end
+ def modify_for_passwords(attributes)
+ content_type.password_fields.each do |field|
+ uncrypted_password = attributes.delete(field.name.to_sym)
+ attributes[:"#{field.name}_hash"] = BCrypt::Password.create(uncrypted_password)
+ end
+ end
+
def file_size(path)
return nil if path.blank?
locomotive/steam/entities/content_entry.rb b/lib/locomotive/steam/entities/content_entry.rb +7 -0
@@ @@ -1,4 +1,5 @@
require 'chronic'
+ require 'bcrypt'
module Locomotive::Steam
@@ @@ -131,6 +132,12 @@ module Locomotive::Steam
_cast_convertor(field.name, &:to_f)
end
+ def _cast_password(field)
+ _cast_convertor(:"#{field.name}_hash") do |value|
+ value.blank? ? nil : BCrypt::Password.new(value)
+ end
+ end
+
def _cast_file(field)
_cast_convertor(field.name) do |value, locale|
if value.respond_to?(:url)
locomotive/steam/entities/content_type.rb b/lib/locomotive/steam/entities/content_type.rb +1 -0
@@ @@ -8,6 +8,7 @@ module Locomotive::Steam
def_delegator :fields, :associations, :association_fields
def_delegator :fields, :selects, :select_fields
def_delegator :fields, :files, :file_fields
+ def_delegator :fields, :passwords, :password_fields
def_delegator :fields, :default, :fields_with_default
def initialize(attributes = {})
locomotive/steam/initializers.rb b/lib/locomotive/steam/initializers.rb +1 -0
@@ @@ -1,3 +1,4 @@
require_relative 'initializers/sprockets.rb'
require_relative 'initializers/i18n.rb'
require_relative 'initializers/dragonfly.rb'
+ require_relative 'initializers/warden_strategy.rb'
locomotive/steam/initializers/warden_strategy.rb b/lib/locomotive/steam/initializers/warden_strategy.rb +40 -0
@@ @@ -0,0 +1,40 @@
+ # require 'warden'
+
+ # Warden::Strategies.add(:steam_password) do
+
+ # def valid?
+ # params['auth'].present?
+ # end
+
+ # def authenticate!
+ # puts "[Warden][steam_password] authenticate!"
+ # puts params.inspect
+
+ # entry = nil
+
+ # if type = repositories.content_type.by_slug(params['auth_content_type'])
+ # id_field = params['auth_id_field'].try(:to_sym) || :email
+ # entry = repositories.content_entry.with(type).all(id_field => params['auth_id']).first
+
+ # entry = nil unless entry.password == params['auth_password']
+ # end
+
+ # puts "good? #{entry.password == params['auth_password']}"
+
+ # entry.nil? ? fail!("Could not log in") : success!(entry)
+
+ # # puts entry.name.inspect
+
+ # # # TODO
+ # # u = User.authenticate(params['username'], params['password'])
+ # # u.nil? ? fail!("Could not log in") : success!(u)
+ # end
+
+ # def services
+ # @services ||= env['steam.services']
+ # end
+
+ # def repositories
+ # @repositories ||= services.repositories
+ # end
+ # end
locomotive/steam/middlewares/auth.rb b/lib/locomotive/steam/middlewares/auth.rb +92 -0
@@ @@ -0,0 +1,92 @@
+ module Locomotive::Steam
+ module Middlewares
+
+ # Process all the authentication actions:
+ # - sign in
+ # - new reset password
+ # - reset password
+ # - sign out
+ #
+ 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
+
+ # if ACTIONS.include?(params[:auth])
+ # sign_in if params[:auth] == 'sign_in'
+ # end
+ end
+
+ private
+
+ def load_authenticated_entry
+ if (entry_id = request.session[:authenticated_entry_id]) &&
+ (entry_type = request.session[:authenticated_entry_type])
+
+ env['authenticated_entry'] = find_entry(entry_type, entry_id)
+
+ liquid_assigns["current_#{entry_type.singularize}"] = decorate_entry(env['authenticated_entry'])
+ end
+ 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
+ end
+ end
+
+ def authenticated?
+ !!env['authenticated_entry']
+ end
+
+ def sign_out
+ return unless authenticated?
+
+ type = request.session[:authenticated_entry_type]
+
+ request.session[:authenticated_entry_id] = nil
+ request.session[:authenticated_entry_type] = nil
+
+ liquid_assigns["current_#{type.singularize}"] = nil
+ liquid_assigns['auth_signed_out'] = 'auth_signed_out'
+ end
+
+ def sign_in
+ unless authenticated?
+ if type = repositories.content_type.by_slug(params[:auth_content_type])
+ id_field = params[:auth_id_field] || :email
+
+ entry = repositories.content_entry.with(type).all(id_field => params[:auth_id]).first
+
+ if entry && entry.password == params[:auth_password]
+ request.session[:authenticated_entry_id] = entry._id
+ request.session[:authenticated_entry_type] = type.slug
+
+ liquid_assigns["current_#{type.slug.singularize}"] = decorate_entry(entry)
+
+ 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
+ end
+ end
+
+ end
+
+ end
+ end
locomotive/steam/middlewares/templatized_page.rb b/lib/locomotive/steam/middlewares/templatized_page.rb +1 -6
@@ @@ -38,7 +38,7 @@ module Locomotive::Steam
# don't accept a non localized entry in a locale other than the default one
return nil if type.localized_names.count == 0 && locale.to_s != default_locale.to_s
- decorate(content_entry_repository.with(type).by_slug(slug))
+ decorate_entry(content_entry_repository.with(type).by_slug(slug))
else
nil
end
@@ @@ -56,11 +56,6 @@ module Locomotive::Steam
services.page_finder.find('404')
end
- def decorate(entry)
- return nil if entry.nil?
- Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale, default_locale)
- end
-
end
end
end
locomotive/steam/middlewares/thread_safe.rb b/lib/locomotive/steam/middlewares/thread_safe.rb +9 -0
@@ @@ -29,6 +29,10 @@ module Locomotive::Steam::Middlewares
@services ||= env.fetch('steam.services')
end
+ def repositories
+ @repositories ||= services.repositories
+ end
+
def request
@request ||= env.fetch('steam.request')
end
@@ @@ -65,6 +69,11 @@ module Locomotive::Steam::Middlewares
!!env['steam.live_editing']
end
+ def decorate_entry(entry)
+ return nil if entry.nil?
+ Locomotive::Steam::Decorators::I18nDecorator.new(entry, locale, default_locale)
+ end
+
end
end
locomotive/steam/middlewares/warden.rb b/lib/locomotive/steam/middlewares/warden.rb +20 -0
@@ @@ -0,0 +1,20 @@
+ # module Locomotive::Steam
+ # module Middlewares
+
+ # class Warden
+
+ # def initialize(app)
+ # @warden = ::Warden::Manager.new(app) do |manager|
+ # manager.default_strategies :steam_password
+ # manager.failure_app = app
+ # end
+ # end
+
+ # def call(env)
+ # @warden.call(env)
+ # end
+
+ # end
+
+ # end
+ # end
locomotive/steam/repositories/content_type_field_repository.rb b/lib/locomotive/steam/repositories/content_type_field_repository.rb +4 -0
@@ @@ -26,6 +26,10 @@ module Locomotive
query { where(type: :file) }.all
end
+ def passwords
+ query { where(type: :password) }.all
+ end
+
def dates_and_date_times
query { where(k(:type, :in) => %i(date date_time)) }.all
end
locomotive/steam/server.rb b/lib/locomotive/steam/server.rb +2 -0
@@ @@ -58,9 +58,11 @@ module Locomotive::Steam
Middlewares::UrlRedirection,
Middlewares::Robots,
Middlewares::Timezone,
+ # Middlewares::Warden,
Middlewares::EntrySubmission,
Middlewares::Locale,
Middlewares::LocaleRedirection,
+ Middlewares::Auth,
Middlewares::PrivateAccess,
Middlewares::Path,
Middlewares::Page,
locomotive/steam/version.rb b/lib/locomotive/steam/version.rb +1 -1
@@ @@ -3,6 +3,6 @@
# 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0
module Locomotive
module Steam
- VERSION = '1.2.0.rc3'
+ VERSION = '1.3.0'
end
end
locomotivecms_steam.gemspec +2 -0
@@ @@ -26,12 +26,14 @@ Gem::Specification.new do |spec|
spec.add_dependency 'morphine', '~> 0.1.1'
spec.add_dependency 'httparty', '~> 0.13.6'
spec.add_dependency 'chronic', '~> 0.10.2'
+ spec.add_dependency 'bcrypt', '~> 3.1.11'
spec.add_dependency 'rack-rewrite', '~> 1.5.1'
spec.add_dependency 'rack-cache', '~> 1.6.1'
spec.add_dependency 'dragonfly', '~> 1.0.12'
spec.add_dependency 'moneta', '~> 0.8.0'
spec.add_dependency 'rack_csrf', '~> 2.5.0'
+ spec.add_dependency 'warden', '~> 1.2.6'
spec.add_dependency 'sprockets', '~> 3.5.2'
spec.add_dependency 'sass', '~> 3.4.21'
spec/fixtures/default/app/content_types/accounts.yml +59 -0
@@ @@ -0,0 +1,59 @@
+ # Human readable name of this type
+ name: Accounts
+
+ # Lowercase, underscored handle used to access this type
+ slug: accounts
+
+ # Explanatory text displayed in the back-office
+ description: A description of the content type for the editors
+
+ # Slug of field used to identify entries by default, such as the title
+ label_field_name: name
+
+ # Valid values: manually, created_at, updated_at, or the slug of any field
+ order_by: manually
+
+ # Valid values: asc (ascending) and desc (descending). Set to asc by default.
+ # order_direction: asc
+
+ # Specify a field slug to group entries by that field in the back-office.
+ # group_by: <your field>
+
+ # Activate public 'create' API (e.g for a contact form)
+ # public_submission_enabled: false
+
+ # Array of emails to be notified of new entries made with the public API
+ # public_submission_accounts: ['john@example.com']
+
+ # Control the display of the content type in the back-office.
+ # display_settings:
+ # seo: false # display the SEO tab for the content entries
+ # advanced: false # display the Advanced tab for the content entries
+ # position: 1 # position in the sidebar menu
+ # hidden: false # hidden for authors?
+
+ # By default, the back-office displays the _label property (see label_field_name) of the content entry. This can be modified by writing your own Liquid template below:
+ # entry_template: '<a href="{{ link }}">{{ entry._label }}</a>' # The default template
+
+ # A list describing each field
+ fields:
+ - name: # The lowercase, underscored name of the field
+ label: Name # Human readable name of the field
+ type: string
+ required: true
+ hint: Explanatory text displayed in the back office
+ localized: false
+
+ - email: # The lowercase, underscored name of the field
+ label: Email # Human readable name of the field
+ type: email
+ required: false
+ hint: Explanatory text displayed in the back office
+ localized: false
+
+ - password: # The lowercase, underscored name of the field
+ label: Password # Human readable name of the field
+ type: password
+ required: false
+ hint: Explanatory text displayed in the back office
+ localized: false
spec/fixtures/default/app/views/pages/account/me.liquid +15 -0
@@ @@ -0,0 +1,15 @@
+ ---
+ title: Account profile
+ listed: false
+ published: true
+ handle: me
+ ---
+ {% extends 'index' %}
+
+ {% block content %}
+
+ {% if current_account %}
+ <p>My name is {{ current_account.name }} and I'm logged in!</p>
+ {% endif %}
+
+ {% endblock %}
spec/fixtures/default/app/views/pages/account/sign_in.liquid +49 -0
@@ @@ -0,0 +1,49 @@
+ ---
+ title: Sign in
+ listed: false
+ published: true
+ handle: sign_in
+ ---
+ {% extends 'index' %}
+
+ {% block content %}
+
+ <h1>Sign in</h1>
+
+ {% if current_account %}
+ <div class="alert alert-warning">
+ You're already authenticated!
+ </div>
+ {% else %}
+
+ <form action="{% path_to 'sign_in' %}" method="POST">
+ <input type="hidden" name="auth" value="sign_in" />
+ <input type="hidden" name="auth_content_type" value="accounts" />
+ <input type="hidden" name="auth_id_field" value="email" />
+ <input type="hidden" name="auth_password_field" value="password" />
+
+ {% if auth_signed_out %}
+ <div class="alert alert-info">
+ {{ auth_signed_out | translate }}
+ </div>
+ {% endif %}
+
+ {% if auth_wrong_credentials %}
+ <div class="alert alert-warning">
+ {{ auth_wrong_credentials | translate }}
+ </div>
+ {% endif %}
+
+ <div class="form-group">
+ <label for="auth-email">Your E-mail</label>
+ <input type="email" name="auth_id" class="form-control" id="auth-email" placeholder="Email">
+ </div>
+ <div class="form-group">
+ <label for="auth-password">Password</label>
+ <input type="password" name="auth_password" class="form-control" id="auth-password" placeholder="Password">
+ </div>
+ <button type="submit" class="btn btn-default">Sign in</button>
+ </form>
+ {% endif %}
+
+ {% endblock %}
spec/fixtures/default/config/translations.yml +9 -1
@@ @@ -1,3 +1,11 @@
powered_by:
en: Powered by
- fr: Propulsé par
\ No newline at end of file
+ fr: Propulsé par
+
+ auth_wrong_credentials:
+ en: Your email and/or password are incorrect
+ fr: Votre email et/ou mot de passe sont incorrects
+
+ auth_signed_out:
+ en: You've been signed out
+ fr: Vous avez été déconnecté(e)
spec/fixtures/default/data/accounts.yml +3 -0
@@ @@ -0,0 +1,3 @@
+ - "John":
+ email: 'john@doe.net'
+ password: 'easyone'
spec/integration/server/auth_spec.rb +95 -0
@@ @@ -0,0 +1,95 @@
+ require File.dirname(__FILE__) + '/../integration_helper'
+
+ describe 'Authentication' do
+
+ include Rack::Test::Methods
+
+ def app
+ run_server
+ end
+
+ describe 'sign in action' do
+
+ it 'renders the form' do
+ get '/account/sign-in'
+ expect(last_response.body).to include '/account/sign-in'
+ expect(last_response.body).not_to include "You've been signed out"
+ end
+
+ describe 'press the sign in button' do
+
+ let(:params) { {
+ auth_action: 'sign_in',
+ auth_content_type: 'accounts',
+ auth_id_field: 'email',
+ auth_password_field: 'password',
+ auth_id: 'john@doe.net',
+ auth_password: 'easyone',
+ auth_callback: '/account/me'
+ } }
+
+ it 'redirects to the callback' do
+ sign_in(params)
+ expect(last_response.status).to eq 301
+ expect(last_response.location).to eq '/account/me'
+ end
+
+ it 'displays the profile page as described in the params' do
+ sign_in(params, true)
+ expect(last_response.body).to include "My name is John and I'm logged in!"
+ end
+
+ context 'wrong credentials' do
+
+ let(:params) { {
+ auth_action: 'sign_in',
+ auth_content_type: 'accounts',
+ auth_id_field: 'email',
+ auth_password_field: 'password',
+ auth_id: 'john@doe.net',
+ auth_password: 'dontrememberit',
+ auth_callback: '/account/me'
+ } }
+
+ it 'renders the sign in page with an error message' do
+ sign_in(params)
+ expect(last_response.status).to eq 200
+ expect(last_response.body).to include '/account/sign-in'
+ expect(last_response.body).to include 'Your email and/or password are incorrect'
+ end
+
+ end
+
+ def sign_in(params, follow_redirect = false)
+ post '/account/sign-in', params
+ follow_redirect! if follow_redirect
+ last_response
+ end
+
+ end
+
+ end
+
+ describe 'sign out action' do
+
+ let(:params) { {
+ auth_action: 'sign_out',
+ auth_content_type: 'accounts'
+ } }
+
+ let(:rack_session) { {
+ 'authenticated_entry_type' => 'accounts',
+ 'authenticated_entry_id' => 'john'
+ } }
+
+ it 'displays the profile page as described in the params' do
+ post '/account/sign-in', params, { 'rack.session' => rack_session }
+ expect(last_response.body).to include "You've been signed out"
+ expect(last_response.body).not_to include "You're already authenticated!"
+ end
+
+ end
+
+ end
+
+