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 |
| + | |
| + | |