quo_vadis 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quo_vadis (1.0.1)
4
+ quo_vadis (1.0.2)
5
5
  bcrypt-ruby (~> 2.1.4)
6
6
  rails (~> 3.0)
7
7
 
data/README.md CHANGED
@@ -8,11 +8,11 @@ Features:
8
8
  * No surprises: it does what you expect.
9
9
  * Easy to customise.
10
10
  * Uses BCrypt to encrypt passwords.
11
- * Sign in, sign out, authenticate actions.
11
+ * Sign in, sign out, forgotten password, authenticate actions.
12
12
 
13
13
  Forthcoming features:
14
14
 
15
- * Handle forgotten-details.
15
+ * Generate the views for you.
16
16
  * Let you choose which model(s) to authenticate (currently `User`).
17
17
  * Let you choose the identification field (currently `username`).
18
18
  * Remember authenticated user across browser sessions.
@@ -35,9 +35,9 @@ What it doesn't and won't do:
35
35
 
36
36
  If this takes you more than 5 minutes, you can have your money back ;)
37
37
 
38
- Install and run the generator: add `gem 'quo_vadis'` to your Gemfile and run `rails generate quo_vadis:install`.
38
+ Install and run the generator: add `gem 'quo_vadis'` to your Gemfile, run `bundle install`, then `rails generate quo_vadis:install`.
39
39
 
40
- Edit and run the generated migration to add authentication columns: `rake db:migrate`. Note the migration (currently) assumes you already have a `User` model.
40
+ Edit and run the generated migration to add the authentication columns: `rake db:migrate`. Note the migration (currently) assumes you already have a `User` model.
41
41
 
42
42
  In your `User` model, add `authenticates`:
43
43
 
@@ -45,7 +45,7 @@ In your `User` model, add `authenticates`:
45
45
  authenticates
46
46
  end
47
47
 
48
- Note Quo Vadis validates the presence of the password, but it's up to you to add any other validations you want.
48
+ Note Quo Vadis validates the presence and uniqueness of the username, and the presence of the password, but it's up to you to add any other validations you want.
49
49
 
50
50
  Use `:authenticate` in a `before_filter` to protect your controllers' actions. For example:
51
51
 
@@ -56,11 +56,52 @@ Use `:authenticate` in a `before_filter` to protect your controllers' actions.
56
56
  Write the sign-in view. Your sign-in form must:
57
57
 
58
58
  * be in `app/views/sessions/new.html.:format`
59
- * post the parameters `:username` and `:password` to `sign_in_url`
59
+ * POST the parameters `:username` and `:password` to `sign_in_url`
60
60
 
61
61
  You have to write the view yourself because you'd inevitably want to change whatever markup I generated for you.
62
62
 
63
- In your layout, use `current_user` to retrieve the signed-in user, and `sign_in_path` and `sign_out_path` as appropriate.
63
+ Remember to serve your sign in form over HTTPS -- to avoid [the credentials being stolen](http://blog.jgc.org/2011/01/code-injected-to-steal-passwords-in.html).
64
+
65
+ In your layout, use `current_user` to retrieve the signed-in user; and `sign_in_path`, `sign_out_path`, and `forgotten_sign_in_path` as appropriate.
66
+
67
+
68
+ ## Forgotten Password
69
+
70
+ Here's the workflow:
71
+
72
+ 1. [Sign in page] The user clicks the "I've forgotten my password" link.
73
+ 2. [Forgotten password page] The user enters their username in the form and submits it.
74
+ 3. Quo Vadis emails the user a message with a change-password link. The link is valid for 3 hours.
75
+ 4. [The email] The user clicks the link.
76
+ 5. [Change password page] The user types in a new password and saves it.
77
+ 6. Quo Vadis changes the user's password and signs the user in.
78
+
79
+ It'll take you about 5 minutes to implement this.
80
+
81
+ On your sign-in page, link to the forgotten-password view at `forgotten_sign_in_url`.
82
+
83
+ Write the forgotten-password view. The form must:
84
+
85
+ * be in `app/views/sessions/forgotten.html.:format`
86
+ * POST the parameter `:username` to `forgotten_sign_in_url`
87
+
88
+ Now write the mailer view, i.e. the email which will be sent to your forgetful users. The view must:
89
+
90
+ * be at `app/views/quo_vadis/notifier/change_password.text.erb`
91
+ * render `@url` somewhere (this is the link the user clicks to go to the change-password page)
92
+
93
+ You can also refer to `@username` in the email view.
94
+
95
+ Configure the email's from address in `config/initializers/quo_vadis.rb`.
96
+
97
+ Configure the default host so ActionMailer can generate the URL. In `config/environments/<env>.rb`:
98
+
99
+ config.action_mailer.default_url_options = {:host => 'yourdomain.com'}
100
+
101
+ Finally, write the change-password page. The form must:
102
+
103
+ * be in `app/views/sessions/edit.html.:format`
104
+ * PUT the parameter `:password` to `change_password_url(params[:token])`
64
105
 
65
106
 
66
107
  ## Customisation
@@ -3,7 +3,7 @@ module ControllerMixin
3
3
  base.helper_method :current_user
4
4
  end
5
5
 
6
- private # TODO: does this mark them as private once mixed in?
6
+ private
7
7
 
8
8
  def current_user=(user)
9
9
  session[:current_user_id] = user ? user.id : nil
@@ -16,7 +16,7 @@ module ControllerMixin
16
16
  def authenticate
17
17
  unless current_user
18
18
  session[:quo_vadis_original_url] = request.fullpath
19
- flash[:notice] = t('quo_vadis.flash.before_sign_in') unless t('quo_vadis.flash.before_sign_in').blank?
19
+ flash[:notice] = t('quo_vadis.flash.sign_in.before') unless t('quo_vadis.flash.sign_in.before').blank?
20
20
  redirect_to sign_in_url
21
21
  end
22
22
  end
@@ -1,26 +1,24 @@
1
1
  class QuoVadis::SessionsController < ApplicationController
2
2
  layout :quo_vadis_layout
3
3
 
4
- # sign in
4
+ # GET sign_in_path
5
5
  def new
6
6
  render 'sessions/new'
7
7
  end
8
8
 
9
- # sign in
9
+ # POST sign_in_path
10
10
  def create
11
11
  if user = User.authenticate(params[:username], params[:password])
12
- self.current_user = user
13
- QuoVadis.signed_in_hook user, self
14
- flash[:notice] = t('quo_vadis.flash.after_sign_in') unless t('quo_vadis.flash.after_sign_in').blank?
15
- redirect_to QuoVadis.signed_in_url(user, original_url)
12
+ flash[:notice] = t('quo_vadis.flash.sign_in.after') unless t('quo_vadis.flash.sign_in.after').blank?
13
+ sign_in user
16
14
  else
17
15
  QuoVadis.failed_sign_in_hook self
18
- flash.now[:alert] = t('quo_vadis.flash.failed_sign_in') unless t('quo_vadis.flash.failed_sign_in').blank?
16
+ flash.now[:alert] = t('quo_vadis.flash.sign_in.failed') unless t('quo_vadis.flash.sign_in.failed').blank?
19
17
  render 'sessions/new'
20
18
  end
21
19
  end
22
20
 
23
- # sign out
21
+ # GET sign_out_path
24
22
  def destroy
25
23
  QuoVadis.signed_out_hook current_user, self
26
24
  self.current_user = nil
@@ -28,14 +26,73 @@ class QuoVadis::SessionsController < ApplicationController
28
26
  redirect_to QuoVadis.signed_out_url
29
27
  end
30
28
 
29
+ # GET forgotten_sign_in_path
30
+ # POST forgotten_sign_in_path
31
+ def forgotten
32
+ if request.get?
33
+ render 'sessions/forgotten'
34
+ elsif request.post?
35
+ if (user = User.where(:username => params[:username]).first)
36
+ if user.email.present?
37
+ user.generate_token
38
+ QuoVadis::Notifier.change_password(user).deliver
39
+ flash[:notice] = t('quo_vadis.flash.forgotten.sent_email') unless t('quo_vadis.flash.forgotten.sent_email').blank?
40
+ redirect_to :root
41
+ else
42
+ flash.now[:alert] = t('quo_vadis.flash.forgotten.no_email') unless t('quo_vadis.flash.forgotten.no_email').blank?
43
+ render 'sessions/forgotten'
44
+ end
45
+ else
46
+ flash.now[:alert] = t('quo_vadis.flash.forgotten.unknown') unless t('quo_vadis.flash.forgotten.unknown').blank?
47
+ render 'sessions/forgotten'
48
+ end
49
+ end
50
+ end
51
+
52
+ # GET change_password_path /sign-in/change-password/:token
53
+ def edit
54
+ if User.valid_token(params[:token]).first
55
+ render 'sessions/edit'
56
+ else
57
+ invalid_token
58
+ end
59
+ end
60
+
61
+ # PUT change_password_path /sign-in/change-password/:token
62
+ def update
63
+ if (user = User.valid_token(params[:token]).first)
64
+ user.password = params[:password]
65
+ if user.save
66
+ user.clear_token
67
+ flash[:notice] = t('quo_vadis.flash.forgotten.password_changed') unless t('quo_vadis.flash.forgotten.password_changed').blank?
68
+ sign_in user
69
+ else
70
+ render 'sessions/edit'
71
+ end
72
+ else
73
+ invalid_token
74
+ end
75
+ end
76
+
31
77
  private
32
78
 
79
+ def sign_in(user)
80
+ self.current_user = user
81
+ QuoVadis.signed_in_hook user, self
82
+ redirect_to QuoVadis.signed_in_url(user, original_url)
83
+ end
84
+
33
85
  def original_url
34
86
  url = session[:quo_vadis_original_url]
35
87
  session[:quo_vadis_original_url] = nil
36
88
  url
37
89
  end
38
90
 
91
+ def invalid_token
92
+ flash[:alert] = t('quo_vadis.flash.forgotten.invalid_token') unless t('quo_vadis.flash.forgotten.invalid_token').blank?
93
+ redirect_to forgotten_sign_in_url
94
+ end
95
+
39
96
  def quo_vadis_layout
40
97
  QuoVadis.layout
41
98
  end
@@ -0,0 +1,11 @@
1
+ module QuoVadis
2
+ class Notifier < ActionMailer::Base
3
+
4
+ def change_password(user)
5
+ @username = user.username
6
+ @url = change_password_url user.token
7
+ mail :to => user.email, :from => QuoVadis.from, :subject => QuoVadis.subject
8
+ end
9
+
10
+ end
11
+ end
@@ -13,9 +13,11 @@ module ModelMixin
13
13
  attr_reader :password
14
14
  attr_protected :password_digest
15
15
 
16
- validates :username, :presence => true, :uniqueness => true
17
- validates :password, :on => :create, :presence => true
18
- validates :password_digest, :presence => true
16
+ validates :username, :presence => true, :uniqueness => true
17
+ validates :password, :presence => true, :if => Proc.new { |u| u.changed.include?('password_digest') }
18
+ validates :password_digest, :presence => true
19
+
20
+ scope :valid_token, lambda { |token| where("token = ? AND token_created_at > ?", token, 3.hours.ago) }
19
21
 
20
22
  instance_eval <<-END
21
23
  def authenticate(username, plain_text_password)
@@ -36,9 +38,27 @@ module ModelMixin
36
38
  self.password_digest = BCrypt::Password.create plain_text_password
37
39
  end
38
40
 
41
+ def generate_token
42
+ begin
43
+ self.token = url_friendly_token
44
+ end while self.class.exists?(:token => token)
45
+ self.token_created_at = Time.now.utc
46
+ save
47
+ end
48
+
49
+ def clear_token
50
+ update_attributes :token => nil, :token_created_at => nil
51
+ end
52
+
39
53
  def has_matching_password?(plain_text_password)
40
54
  BCrypt::Password.new(password_digest) == plain_text_password
41
55
  end
56
+
57
+ private
58
+
59
+ def url_friendly_token
60
+ ActiveSupport::SecureRandom.base64(10).tr('+/=', 'xyz')
61
+ end
42
62
  end
43
63
 
44
64
  end
@@ -48,6 +48,17 @@ QuoVadis.configure do |config|
48
48
  config.signed_out_hook = nil
49
49
 
50
50
 
51
+ #
52
+ # Forgotten-password Mailer
53
+ #
54
+
55
+ # From whom the forgotten-password email should be sent.
56
+ config.from = 'noreply@example.com'
57
+
58
+ # Subject of the forgotten-password email.
59
+ config.subject = 'Change your password'
60
+
61
+
51
62
  #
52
63
  # Miscellaneous
53
64
  #
@@ -1,7 +1,16 @@
1
1
  en:
2
2
  quo_vadis:
3
3
  flash:
4
- before_sign_in: 'Please sign in first.'
5
- after_sign_in: 'You have successfully signed in.'
6
- failed_sign_in: 'Sorry, we did not recognise you.'
4
+ sign_in:
5
+ before: 'Please sign in first.'
6
+ after: 'You have successfully signed in.'
7
+ failed: 'Sorry, we did not recognise you.'
8
+
7
9
  sign_out: 'You have successfully signed out.'
10
+
11
+ forgotten:
12
+ unknown: "Sorry, we did not recognise you."
13
+ no_email: "Sorry, we don't have an email address for you."
14
+ sent_email: "We've emailed you a link where you can change your password."
15
+ invalid_token: "Sorry, this link isn't valid anymore."
16
+ password_changed: "You have successfully changed your password and you're now signed in."
data/config/routes.rb CHANGED
@@ -1,7 +1,13 @@
1
- Rails.application.routes.draw do |map|
1
+ Rails.application.routes.draw do
2
2
  scope :module => 'quo_vadis' do
3
- get 'sign-in' => 'sessions#new', :as => 'sign_in'
4
- post 'sign-in' => 'sessions#create', :as => 'sign_in'
5
- get 'sign-out' => 'sessions#destroy', :as => 'sign_out'
3
+ get 'sign-in' => 'sessions#new', :as => 'sign_in'
4
+ post 'sign-in' => 'sessions#create', :as => 'sign_in'
5
+ get 'sign-out' => 'sessions#destroy', :as => 'sign_out'
6
+ get 'sign-in/forgotten' => 'sessions#forgotten', :as => 'forgotten_sign_in'
7
+ post 'sign-in/forgotten' => 'sessions#forgotten', :as => 'forgotten_sign_in'
8
+ constraints :token => /.+/ do
9
+ get 'sign-in/change-password/:token' => 'sessions#edit', :as => 'change_password'
10
+ put 'sign-in/change-password/:token' => 'sessions#update', :as => 'change_password'
11
+ end
6
12
  end
7
13
  end
@@ -1,11 +1,18 @@
1
1
  class AddAuthenticationToUsers < ActiveRecord::Migration
2
2
  def self.up
3
- add_column :users, :username, :string # for user identification
4
- add_column :users, :password_digest, :string
3
+ add_column :users, :username, :string # for user identification
4
+ add_column :users, :password_digest, :string
5
+
6
+ add_column :users, :email, :string # for forgotten-credentials
7
+ add_column :users, :token, :string # for forgotten-credentials
8
+ add_column :users, :token_created_at, :string # for forgotten-credentials
5
9
  end
6
10
 
7
11
  def self.down
8
12
  remove_column :users, :username
9
13
  remove_column :users, :password_digest
14
+ remove_column :users, :email
15
+ remove_column :users, :token
16
+ remove_column :users, :token_created_at
10
17
  end
11
18
  end
@@ -1,3 +1,3 @@
1
1
  module QuoVadis
2
- VERSION = '1.0.1'
2
+ VERSION = '1.0.2'
3
3
  end
data/lib/quo_vadis.rb CHANGED
@@ -57,6 +57,19 @@ module QuoVadis
57
57
  end
58
58
 
59
59
 
60
+ #
61
+ # Forgotten-password Mailer
62
+ #
63
+
64
+ # From whom the forgotten-password email should be sent.
65
+ mattr_accessor :from
66
+ @@from = 'noreply@example.com'
67
+
68
+ # Subject of the forgotten-password email.
69
+ mattr_accessor :subject
70
+ @@subject = 'Change your password.'
71
+
72
+
60
73
  #
61
74
  # Miscellaneous
62
75
  #
@@ -0,0 +1,7 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+ require 'rake'
6
+
7
+ Dummy::Application.load_tasks
@@ -0,0 +1,9 @@
1
+ Hello <%= @username %>,
2
+
3
+ We've received a request to change your password. If this wasn't you,
4
+ please ignore this email and your password will be left alone.
5
+
6
+ If you do want to change your password, just click this link (valid for 3 hours):
7
+ <%= @url %>
8
+
9
+ Thanks!
@@ -0,0 +1,11 @@
1
+ <h1>Change your password</h1>
2
+
3
+ <%= form_tag change_password_path(params[:token]), :method => :put do %>
4
+ <p>
5
+ <%= label_tag :password %>
6
+ <%= password_field_tag :password %>
7
+ </p>
8
+ <p>
9
+ <%= submit_tag 'Change my password' %>
10
+ </p>
11
+ <% end %>
@@ -0,0 +1,13 @@
1
+ <h1>Forgotten your password?</h1>
2
+
3
+ <p>Don't worry, it happens to the best of us. Just tell us who you are, and we'll send you an email explaining how to change your password.</p>
4
+
5
+ <%= form_tag forgotten_sign_in_path do %>
6
+ <p>
7
+ <%= label_tag :username %>
8
+ <%= text_field_tag :username %>
9
+ </p>
10
+ <p>
11
+ <%= submit_tag 'Send me that email' %>
12
+ </p>
13
+ <% end %>
@@ -25,6 +25,8 @@ Dummy::Application.configure do
25
25
  # ActionMailer::Base.deliveries array.
26
26
  config.action_mailer.delivery_method = :test
27
27
 
28
+ config.action_mailer.default_url_options = {:host => 'www.example.com'}
29
+
28
30
  # Use SQL instead of Active Record's schema dumper when creating the test database.
29
31
  # This is necessary if your schema can't be completely dumped by the schema dumper,
30
32
  # like if you have constraints or database-specific column types
@@ -0,0 +1,58 @@
1
+ QuoVadis.configure do |config|
2
+
3
+ #
4
+ # Redirection URLs
5
+ #
6
+
7
+ # The URL to redirect the user to after s/he signs in.
8
+ # Use a proc if the URL depends on the user. E.g.:
9
+ #
10
+ # config.signed_in_url = Proc.new do |user|
11
+ # user.admin? ? :admin : :root
12
+ # end
13
+ #
14
+ # See also `:override_original_url`.
15
+ config.signed_in_url = :root
16
+
17
+ # Whether the `:signed_in_url` should override the URL the user was trying
18
+ # to reach when they were made to authenticate.
19
+ config.override_original_url = false
20
+
21
+ # The URL to redirect the user to after s/he signs out.
22
+ config.signed_out_url = :root
23
+
24
+
25
+ #
26
+ # Hooks
27
+ #
28
+
29
+ # Code to run when the user has signed in. E.g.:
30
+ #
31
+ # config.signed_in_hook = Proc.new do |user, controller|
32
+ # user.increment! :sign_in_count # assuming this attribute exists
33
+ # end
34
+ config.signed_in_hook = nil
35
+
36
+ # Code to run when someone has tried but failed to sign in. E.g.:
37
+ #
38
+ # config.failed_sign_in_hook = Proc.new do |controller|
39
+ # logger.info "Failed sign in from #{controller.request.remote_ip}"
40
+ # end
41
+ config.failed_sign_in_hook = nil
42
+
43
+ # Code to run just before the user has signed out. E.g.:
44
+ #
45
+ # config.signed_out_hook = Proc.new do |user, controller|
46
+ # controller.session.reset
47
+ # end
48
+ config.signed_out_hook = nil
49
+
50
+
51
+ #
52
+ # Miscellaneous
53
+ #
54
+
55
+ # Layout for the sign-in view. Pass a string or a symbol.
56
+ config.layout = 'application'
57
+
58
+ end
@@ -0,0 +1,7 @@
1
+ en:
2
+ quo_vadis:
3
+ flash:
4
+ before_sign_in: 'Please sign in first.'
5
+ after_sign_in: 'You have successfully signed in.'
6
+ failed_sign_in: 'Sorry, we did not recognise you.'
7
+ sign_out: 'You have successfully signed out.'
@@ -0,0 +1,18 @@
1
+ class AddAuthenticationToUsers < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :users, :username, :string # for user identification
4
+ add_column :users, :password_digest, :string
5
+
6
+ add_column :users, :email, :string # for forgotten-credentials
7
+ add_column :users, :token, :string # for forgotten-credentials
8
+ add_column :users, :token_created_at, :string # for forgotten-credentials
9
+ end
10
+
11
+ def self.down
12
+ remove_column :users, :username
13
+ remove_column :users, :password_digest
14
+ remove_column :users, :email
15
+ remove_column :users, :token
16
+ remove_column :users, :token_created_at
17
+ end
18
+ end
@@ -0,0 +1,33 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # Note that this schema.rb definition is the authoritative source for your
6
+ # database schema. If you need to create the application database on another
7
+ # system, you should be using db:schema:load, not running all the migrations
8
+ # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
+ # you'll amass, the slower it'll run and the greater likelihood for issues).
10
+ #
11
+ # It's strongly recommended to check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(:version => 20110127094709) do
14
+
15
+ create_table "articles", :force => true do |t|
16
+ t.string "title"
17
+ t.text "content"
18
+ t.datetime "created_at"
19
+ t.datetime "updated_at"
20
+ end
21
+
22
+ create_table "users", :force => true do |t|
23
+ t.string "name"
24
+ t.datetime "created_at"
25
+ t.datetime "updated_at"
26
+ t.string "username"
27
+ t.string "password_digest"
28
+ t.string "email"
29
+ t.string "token"
30
+ t.string "token_created_at"
31
+ end
32
+
33
+ end
@@ -99,4 +99,18 @@ class ConfigTest < ActiveSupport::IntegrationCase
99
99
  assert page.has_content?('Sessions layout')
100
100
  end
101
101
 
102
+ test 'mailer from config' do
103
+ QuoVadis.from = 'jim@example.com'
104
+ (user = User.last).generate_token
105
+ email = QuoVadis::Notifier.change_password(user)
106
+ assert_equal ['jim@example.com'], email.from
107
+ end
108
+
109
+ test 'mailer subject config' do
110
+ QuoVadis.subject = 'You idiot!'
111
+ (user = User.last).generate_token
112
+ email = QuoVadis::Notifier.change_password(user)
113
+ assert_equal 'You idiot!', email.subject
114
+ end
115
+
102
116
  end
@@ -0,0 +1,83 @@
1
+ require 'test_helper'
2
+
3
+ class ForgottenTest < ActiveSupport::IntegrationCase
4
+
5
+ teardown do
6
+ Capybara.reset_sessions!
7
+ end
8
+
9
+ test 'user fills in forgotten-password form with invalid username' do
10
+ submit_forgotten_details 'bob'
11
+ assert_equal forgotten_sign_in_path, current_path
12
+ within '.flash.alert' do
13
+ assert page.has_content?("Sorry, we did not recognise you.")
14
+ end
15
+ end
16
+
17
+ test 'user without email requests password-change email' do
18
+ user_factory 'Bob', 'bob', 'secret'
19
+ submit_forgotten_details 'bob'
20
+ assert_equal forgotten_sign_in_path, current_path
21
+ within '.flash.alert' do
22
+ assert page.has_content?("Sorry, we don't have an email address for you.")
23
+ end
24
+ end
25
+
26
+ test 'user can request password-change email' do
27
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
28
+ submit_forgotten_details 'bob'
29
+
30
+ assert_equal root_path, current_path
31
+ within '.flash.notice' do
32
+ assert page.has_content?("We've emailed you a link where you can change your password.")
33
+ end
34
+ assert !ActionMailer::Base.deliveries.empty?
35
+ email = ActionMailer::Base.deliveries.last
36
+ assert_equal ['bob@example.com'], email.to
37
+ assert_equal ['noreply@example.com'], email.from
38
+ assert_equal 'Change your password', email.subject
39
+ # Why doesn't this use the default url option set up in test/test_helper.rb#9?
40
+ assert_match Regexp.new(Regexp.escape(change_password_url User.last.token, :host => 'www.example.com')), email.encoded
41
+ end
42
+
43
+ test 'user can follow emailed link while valid to change password' do
44
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
45
+ submit_forgotten_details 'bob'
46
+
47
+ link_in_email = ActionMailer::Base.deliveries.last.encoded[%r{http://.*}].strip
48
+ visit link_in_email
49
+ fill_in :password, :with => 'topsecret'
50
+ click_button 'Change my password'
51
+ assert_equal root_path, current_path
52
+ within '.flash.notice' do
53
+ assert page.has_content?("You have successfully changed your password and you're now signed in.")
54
+ end
55
+ assert_nil User.last.token
56
+ assert_nil User.last.token_created_at
57
+ end
58
+
59
+ test 'user cannot change password to an invalid one' do
60
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
61
+ submit_forgotten_details 'bob'
62
+
63
+ link_in_email = ActionMailer::Base.deliveries.last.encoded[%r{http://.*}].strip
64
+ visit link_in_email
65
+ fill_in :password, :with => ''
66
+ click_button 'Change my password'
67
+ assert_equal change_password_path(User.last.token), current_path
68
+ end
69
+
70
+ test 'user cannot change password once emailed link is invalid' do
71
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
72
+ submit_forgotten_details 'bob'
73
+ User.last.update_attributes :token_created_at => 1.day.ago
74
+
75
+ link_in_email = ActionMailer::Base.deliveries.last.encoded[%r{http://.*}].strip
76
+ visit link_in_email
77
+ assert_equal forgotten_sign_in_path, current_path
78
+ within '.flash.alert' do
79
+ assert page.has_content?("Sorry, this link isn't valid anymore.")
80
+ end
81
+ end
82
+
83
+ end
@@ -6,14 +6,14 @@ class LocaleTest < ActiveSupport::IntegrationCase
6
6
  Capybara.reset_sessions!
7
7
  end
8
8
 
9
- test 'before_sign_in flash' do
9
+ test 'sign_in.before flash' do
10
10
  visit new_article_path
11
11
  within '.flash' do
12
12
  assert page.has_content?('Please sign in first.')
13
13
  end
14
14
  end
15
15
 
16
- test 'after_sign_in flash' do
16
+ test 'sign_in.after flash' do
17
17
  user_factory 'Bob', 'bob', 'secret'
18
18
  sign_in_as 'bob', 'secret'
19
19
  within '.flash' do
@@ -21,7 +21,7 @@ class LocaleTest < ActiveSupport::IntegrationCase
21
21
  end
22
22
  end
23
23
 
24
- test 'failed_sign_in flash' do
24
+ test 'sign_in.failed flash' do
25
25
  sign_in_as 'bob', 'secret'
26
26
  within '.flash' do
27
27
  assert page.has_content?('Sorry, we did not recognise you.')
@@ -35,9 +35,9 @@ class LocaleTest < ActiveSupport::IntegrationCase
35
35
  end
36
36
  end
37
37
 
38
- test 'before_sign_in flash is optional' do
38
+ test 'sign_in.before flash is optional' do
39
39
  begin
40
- I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:before_sign_in => ''}}}
40
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:sign_in => {:before => ''}}}}
41
41
  visit new_article_path
42
42
  assert page.has_no_css?('div.flash')
43
43
  ensure
@@ -45,10 +45,10 @@ class LocaleTest < ActiveSupport::IntegrationCase
45
45
  end
46
46
  end
47
47
 
48
- test 'after_sign_in flash is optional' do
48
+ test 'sign_in.after flash is optional' do
49
49
  user_factory 'Bob', 'bob', 'secret'
50
50
  begin
51
- I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:after_sign_in => ''}}}
51
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:sign_in => {:after => ''}}}}
52
52
  sign_in_as 'bob', 'secret'
53
53
  assert page.has_no_css?('div.flash')
54
54
  ensure
@@ -56,9 +56,9 @@ class LocaleTest < ActiveSupport::IntegrationCase
56
56
  end
57
57
  end
58
58
 
59
- test 'failed_sign_in flash is optional' do
59
+ test 'sign_in.failed flash is optional' do
60
60
  begin
61
- I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:failed_sign_in => ''}}}
61
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:sign_in => {:failed => ''}}}}
62
62
  sign_in_as 'bob', 'secret'
63
63
  assert page.has_no_css?('div.flash')
64
64
  ensure
@@ -75,4 +75,102 @@ class LocaleTest < ActiveSupport::IntegrationCase
75
75
  I18n.reload!
76
76
  end
77
77
  end
78
+
79
+ test 'forgotten.unknown flash' do
80
+ submit_forgotten_details 'bob'
81
+ within '.flash.alert' do
82
+ assert page.has_content?('Sorry, we did not recognise you.')
83
+ end
84
+ end
85
+
86
+ test 'forgotten.unknown flash is optional' do
87
+ begin
88
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:forgotten => {:unknown => ''}}}}
89
+ submit_forgotten_details 'bob'
90
+ assert page.has_no_css?('div.flash')
91
+ ensure
92
+ I18n.reload!
93
+ end
94
+ end
95
+
96
+ test 'forgotten.no_email flash' do
97
+ user_factory 'Bob', 'bob', 'secret'
98
+ submit_forgotten_details 'bob'
99
+ within '.flash.alert' do
100
+ assert page.has_content?("Sorry, we don't have an email address for you.")
101
+ end
102
+ end
103
+
104
+ test 'forgotten.no_email flash is optional' do
105
+ begin
106
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:forgotten => {:no_email => ''}}}}
107
+ user_factory 'Bob', 'bob', 'secret'
108
+ submit_forgotten_details 'bob'
109
+ assert page.has_no_css?('div.flash')
110
+ ensure
111
+ I18n.reload!
112
+ end
113
+ end
114
+
115
+ test 'forgotten.sent_email flash' do
116
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
117
+ submit_forgotten_details 'bob'
118
+ within '.flash.notice' do
119
+ assert page.has_content?("We've emailed you a link where you can change your password.")
120
+ end
121
+ end
122
+
123
+ test 'forgotten.sent_email flash is optional' do
124
+ begin
125
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:forgotten => {:sent_email => ''}}}}
126
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
127
+ submit_forgotten_details 'bob'
128
+ assert page.has_no_css?('div.flash')
129
+ ensure
130
+ I18n.reload!
131
+ end
132
+ end
133
+
134
+ test 'forgotten.invalid_token flash' do
135
+ visit change_password_path('123')
136
+ within '.flash.alert' do
137
+ assert page.has_content?("Sorry, this link isn't valid anymore.")
138
+ end
139
+ end
140
+
141
+ test 'forgotten.invalid_token flash is optional' do
142
+ begin
143
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:forgotten => {:invalid_token => ''}}}}
144
+ visit change_password_path('123')
145
+ assert page.has_no_css?('div.flash')
146
+ ensure
147
+ I18n.reload!
148
+ end
149
+ end
150
+
151
+ test 'forgotten.password_changed flash' do
152
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
153
+ User.last.generate_token
154
+ visit change_password_path(User.last.token)
155
+ fill_in :password, :with => 'topsecret'
156
+ click_button 'Change my password'
157
+ within '.flash.notice' do
158
+ assert page.has_content?("You have successfully changed your password and you're now signed in.")
159
+ end
160
+ end
161
+
162
+ test 'forgotten.password_changed flash is optional' do
163
+ begin
164
+ I18n.backend.store_translations :en, {:quo_vadis => {:flash => {:forgotten => {:password_changed => ''}}}}
165
+ user_factory 'Bob', 'bob', 'secret', 'bob@example.com'
166
+ User.last.generate_token
167
+ visit change_password_path(User.last.token)
168
+ fill_in :password, :with => 'topsecret'
169
+ click_button 'Change my password'
170
+ assert page.has_no_css?('div.flash')
171
+ ensure
172
+ I18n.reload!
173
+ end
174
+ end
175
+
78
176
  end
data/test/test_helper.rb CHANGED
@@ -6,7 +6,7 @@ require "rails/test_help"
6
6
 
7
7
  ActionMailer::Base.delivery_method = :test
8
8
  ActionMailer::Base.perform_deliveries = true
9
- ActionMailer::Base.default_url_options[:host] = "test.com"
9
+ ActionMailer::Base.default_url_options[:host] = "www.example.com"
10
10
 
11
11
  Rails.backtrace_cleaner.remove_silencers!
12
12
 
@@ -32,8 +32,14 @@ def sign_in_as(username, password)
32
32
  click_button 'Sign in'
33
33
  end
34
34
 
35
- def user_factory(name, username, password)
36
- User.create! :name => name, :username => username, :password => password
35
+ def submit_forgotten_details(username)
36
+ visit forgotten_sign_in_path
37
+ fill_in 'username', :with => username
38
+ click_button 'Send me that email'
39
+ end
40
+
41
+ def user_factory(name, username, password, email = nil)
42
+ User.create! :name => name, :username => username, :password => password, :email => email
37
43
  end
38
44
 
39
45
  def reset_quo_vadis_configuration
@@ -44,4 +50,6 @@ def reset_quo_vadis_configuration
44
50
  QuoVadis.failed_sign_in_hook = nil
45
51
  QuoVadis.signed_out_hook = nil
46
52
  QuoVadis.layout = 'application'
53
+ QuoVadis.from = 'noreply@example.com'
54
+ QuoVadis.subject = 'Change your password'
47
55
  end
@@ -0,0 +1,31 @@
1
+ require 'test_helper'
2
+
3
+ class UserTest < ActiveSupport::TestCase
4
+
5
+ test 'user must have a valid password on create' do
6
+ assert !User.create(:username => 'bob', :password => nil).valid?
7
+ assert !User.create(:username => 'bob', :password => '').valid?
8
+ assert User.create(:username => 'bob', :password => 'secret').valid?
9
+ end
10
+
11
+ test 'user need not supply password when updating other attributes' do
12
+ User.create :username => 'bob', :password => 'secret'
13
+ user = User.last # reload from database so password is nil
14
+ assert_nil user.password
15
+ assert user.update_attributes(:username => 'Robert')
16
+ end
17
+
18
+ test 'user must have a valid password when updating password' do
19
+ user = User.create :username => 'bob', :password => 'secret'
20
+ assert !user.update_attributes(:password => '')
21
+ assert !user.update_attributes(:password => nil)
22
+ assert user.update_attributes(:password => 'topsecret')
23
+ end
24
+
25
+ test 'has_matching_password?' do
26
+ User.create :username => 'bob', :password => 'secret'
27
+ user = User.last
28
+ assert user.has_matching_password?('secret')
29
+ end
30
+
31
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quo_vadis
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 1
8
8
  - 0
9
- - 1
10
- version: 1.0.1
9
+ - 2
10
+ version: 1.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andy Stewart
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-26 00:00:00 +00:00
18
+ date: 2011-01-27 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -110,6 +110,7 @@ files:
110
110
  - Rakefile
111
111
  - app/controllers/controller_mixin.rb
112
112
  - app/controllers/quo_vadis/sessions_controller.rb
113
+ - app/mailers/quo_vadis/notifier.rb
113
114
  - app/models/model_mixin.rb
114
115
  - config/initializers/quo_vadis.rb
115
116
  - config/locales/quo_vadis.en.yml
@@ -121,6 +122,7 @@ files:
121
122
  - lib/quo_vadis/version.rb
122
123
  - quo_vadis.gemspec
123
124
  - test/dummy/.gitignore
125
+ - test/dummy/Rakefile
124
126
  - test/dummy/app/controllers/application_controller.rb
125
127
  - test/dummy/app/controllers/articles_controller.rb
126
128
  - test/dummy/app/helpers/application_helper.rb
@@ -131,6 +133,9 @@ files:
131
133
  - test/dummy/app/views/articles/new.html.erb
132
134
  - test/dummy/app/views/layouts/application.html.erb
133
135
  - test/dummy/app/views/layouts/sessions.html.erb
136
+ - test/dummy/app/views/quo_vadis/notifier/change_password.text.erb
137
+ - test/dummy/app/views/sessions/edit.html.erb
138
+ - test/dummy/app/views/sessions/forgotten.html.erb
134
139
  - test/dummy/app/views/sessions/new.html.erb
135
140
  - test/dummy/config.ru
136
141
  - test/dummy/config/application.rb
@@ -143,13 +148,16 @@ files:
143
148
  - test/dummy/config/initializers/backtrace_silencers.rb
144
149
  - test/dummy/config/initializers/inflections.rb
145
150
  - test/dummy/config/initializers/mime_types.rb
151
+ - test/dummy/config/initializers/quo_vadis.rb
146
152
  - test/dummy/config/initializers/secret_token.rb
147
153
  - test/dummy/config/initializers/session_store.rb
148
154
  - test/dummy/config/locales/en.yml
155
+ - test/dummy/config/locales/quo_vadis.en.yml
149
156
  - test/dummy/config/routes.rb
150
157
  - test/dummy/db/migrate/20110124125037_create_users.rb
151
- - test/dummy/db/migrate/20110124125216_add_authentication_to_users.rb
152
158
  - test/dummy/db/migrate/20110124131535_create_articles.rb
159
+ - test/dummy/db/migrate/20110127094709_add_authentication_to_users.rb
160
+ - test/dummy/db/schema.rb
153
161
  - test/dummy/public/404.html
154
162
  - test/dummy/public/422.html
155
163
  - test/dummy/public/500.html
@@ -169,6 +177,7 @@ files:
169
177
  - test/dummy/tmp/capybara/capybara-20110124135435.html
170
178
  - test/integration/authenticate_test.rb
171
179
  - test/integration/config_test.rb
180
+ - test/integration/forgotten_test.rb
172
181
  - test/integration/helper_test.rb
173
182
  - test/integration/locale_test.rb
174
183
  - test/integration/navigation_test.rb
@@ -177,6 +186,7 @@ files:
177
186
  - test/quo_vadis_test.rb
178
187
  - test/support/integration_case.rb
179
188
  - test/test_helper.rb
189
+ - test/unit/user_test.rb
180
190
  has_rdoc: true
181
191
  homepage: https://github.com/airblade/quo_vadis
182
192
  licenses: []
@@ -213,6 +223,7 @@ specification_version: 3
213
223
  summary: Simple username/password authentication for Rails 3.
214
224
  test_files:
215
225
  - test/dummy/.gitignore
226
+ - test/dummy/Rakefile
216
227
  - test/dummy/app/controllers/application_controller.rb
217
228
  - test/dummy/app/controllers/articles_controller.rb
218
229
  - test/dummy/app/helpers/application_helper.rb
@@ -223,6 +234,9 @@ test_files:
223
234
  - test/dummy/app/views/articles/new.html.erb
224
235
  - test/dummy/app/views/layouts/application.html.erb
225
236
  - test/dummy/app/views/layouts/sessions.html.erb
237
+ - test/dummy/app/views/quo_vadis/notifier/change_password.text.erb
238
+ - test/dummy/app/views/sessions/edit.html.erb
239
+ - test/dummy/app/views/sessions/forgotten.html.erb
226
240
  - test/dummy/app/views/sessions/new.html.erb
227
241
  - test/dummy/config.ru
228
242
  - test/dummy/config/application.rb
@@ -235,13 +249,16 @@ test_files:
235
249
  - test/dummy/config/initializers/backtrace_silencers.rb
236
250
  - test/dummy/config/initializers/inflections.rb
237
251
  - test/dummy/config/initializers/mime_types.rb
252
+ - test/dummy/config/initializers/quo_vadis.rb
238
253
  - test/dummy/config/initializers/secret_token.rb
239
254
  - test/dummy/config/initializers/session_store.rb
240
255
  - test/dummy/config/locales/en.yml
256
+ - test/dummy/config/locales/quo_vadis.en.yml
241
257
  - test/dummy/config/routes.rb
242
258
  - test/dummy/db/migrate/20110124125037_create_users.rb
243
- - test/dummy/db/migrate/20110124125216_add_authentication_to_users.rb
244
259
  - test/dummy/db/migrate/20110124131535_create_articles.rb
260
+ - test/dummy/db/migrate/20110127094709_add_authentication_to_users.rb
261
+ - test/dummy/db/schema.rb
245
262
  - test/dummy/public/404.html
246
263
  - test/dummy/public/422.html
247
264
  - test/dummy/public/500.html
@@ -261,6 +278,7 @@ test_files:
261
278
  - test/dummy/tmp/capybara/capybara-20110124135435.html
262
279
  - test/integration/authenticate_test.rb
263
280
  - test/integration/config_test.rb
281
+ - test/integration/forgotten_test.rb
264
282
  - test/integration/helper_test.rb
265
283
  - test/integration/locale_test.rb
266
284
  - test/integration/navigation_test.rb
@@ -269,3 +287,4 @@ test_files:
269
287
  - test/quo_vadis_test.rb
270
288
  - test/support/integration_case.rb
271
289
  - test/test_helper.rb
290
+ - test/unit/user_test.rb
@@ -1,11 +0,0 @@
1
- class AddAuthenticationToUsers < ActiveRecord::Migration
2
- def self.up
3
- add_column :users, :username, :string # for user identification
4
- add_column :users, :password_digest, :string
5
- end
6
-
7
- def self.down
8
- remove_column :users, :username
9
- remove_column :users, :password_digest
10
- end
11
- end