devise_challenge_questionable 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/README.md +136 -0
- data/Rakefile +1 -0
- data/app/controllers/devise/challenge_questions_controller.rb +106 -0
- data/app/views/devise/challenge_questions/edit.html.erb +64 -0
- data/app/views/devise/challenge_questions/max_challenge_question_attempts_reached.html.erb +11 -0
- data/app/views/devise/challenge_questions/new.html.erb +12 -0
- data/app/views/devise/challenge_questions/show.html.erb +20 -0
- data/app/views/devise/mailer/reset_challenge_questions_instructions.html.erb +8 -0
- data/config/locales/en.yml +13 -0
- data/devise_challenge_questionable.gemspec +29 -0
- data/lib/devise_challenge_questionable.rb +32 -0
- data/lib/devise_challenge_questionable/controllers/helpers.rb +39 -0
- data/lib/devise_challenge_questionable/controllers/url_helpers.rb +24 -0
- data/lib/devise_challenge_questionable/hooks/challenge_questions.rb +5 -0
- data/lib/devise_challenge_questionable/mailer.rb +8 -0
- data/lib/devise_challenge_questionable/model.rb +102 -0
- data/lib/devise_challenge_questionable/rails.rb +21 -0
- data/lib/devise_challenge_questionable/routes.rb +13 -0
- data/lib/devise_challenge_questionable/schema.rb +7 -0
- data/lib/devise_challenge_questionable/version.rb +3 -0
- data/lib/generators/active_record/devise_challenge_questionable_generator.rb +14 -0
- data/lib/generators/active_record/templates/migration.rb +21 -0
- data/lib/generators/devise_challenge_questionable/devise_challenge_questionable_generator.rb +95 -0
- data/lib/generators/devise_challenge_questionable/install_generator.rb +35 -0
- data/lib/generators/devise_challenge_questionable/templates/challenge_question_model.rb +14 -0
- data/lib/generators/devise_challenge_questionable/views_generator.rb +10 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: be8f8f0325e99887de470960d55a3fc9c1b37ec7
|
4
|
+
data.tar.gz: fdaa87f16cfc9d30b8b20406525e3e5b193cbb9e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d81bc9b249d222646417c525eaad2c68f87d404a0c49d777a02dc33bcf677c1bf0e40b6d8e65307c590dd5b793c35caf914d8a366a6fc548e43e8d01f7091066
|
7
|
+
data.tar.gz: dfefac2714845f6aff75bd17c11b0e515a821bf47bb2e9956033d9386bbadc43febe77289038f5d35c566c8344609ee5b797e9f70d808589abb5688715676c8d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012 Dmitrii Golub
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
## Challenge questions plugin for Devise
|
2
|
+
|
3
|
+
This plugin forces a two step login process. After entering username and password, you must then answer a challenge question. Unlike security questions which are typically used when doing things like changing passwords, the challenge question must be answered each time logging in. You can set which users(or preferred resource) need to answer a challenge question
|
4
|
+
## Features
|
5
|
+
|
6
|
+
* configure max challenge question attempts
|
7
|
+
* configure number of challenge questions stored for each {resource}
|
8
|
+
* per {resource} level control if he really need challenge question authentication
|
9
|
+
|
10
|
+
## Configuration
|
11
|
+
|
12
|
+
### Initial Setup
|
13
|
+
|
14
|
+
In a Rails environment, require the gem in your Gemfile:
|
15
|
+
|
16
|
+
gem 'devise_challenge_questionable'
|
17
|
+
|
18
|
+
Once that's done, run:
|
19
|
+
|
20
|
+
bundle install
|
21
|
+
|
22
|
+
|
23
|
+
### Automatic installation
|
24
|
+
|
25
|
+
In order to add challenge questions to a model, run the command:
|
26
|
+
|
27
|
+
bundle exec rails g devise_challenge_questionable MODEL
|
28
|
+
|
29
|
+
bundle exec rails g devise_challenge_questionable:install
|
30
|
+
|
31
|
+
bundle exec rails g devise_challenge_questionable:views {resource}s
|
32
|
+
|
33
|
+
Where MODEL is your model name (e.g. User or Admin). This generator will add `:challenge_questionable` to your model
|
34
|
+
and create a migration in `db/migrate/`, which will add `:reset_challenge_questions_token` and `:challenge_question_failed_attempts` to your table.
|
35
|
+
Finally, run the migration with:
|
36
|
+
|
37
|
+
bundle exec rake db:migrate
|
38
|
+
|
39
|
+
|
40
|
+
### Manual installation
|
41
|
+
|
42
|
+
To manually enable challenge questions for the {Resource} model, you should add the following. Set up relationships. You should already have a devise line so you would just add :challenge_questionable to it. Also, you need to allow accessibility to `:{resource}_challenge_questions_attributes`. Replace {resource} with whatever resource you are using. Typically it would be {resource} or admin.
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
has_many :{resource}_challenge_questions, :validate => true, :inverse_of => :{resource}
|
46
|
+
accepts_nested_attributes_for :{resource}_challenge_questions, :allow_destroy => true
|
47
|
+
|
48
|
+
devise :challenge_questionable
|
49
|
+
|
50
|
+
attr_accessible :{resource}_challenge_questions_attributes
|
51
|
+
```
|
52
|
+
|
53
|
+
You also need to add the `{resource}_challenge_question.rb` Model.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
class {Resource}ChallengeQuestion < ActiveRecord::Base
|
57
|
+
belongs_to :{resource}
|
58
|
+
|
59
|
+
validates :challenge_question, :challenge_answer, :presence => true
|
60
|
+
validates :challenge_answer, :length => { :in => 4..56 }, :format => {:with => /^[\w\s:]*$/, :message => "can not contain special characters"}, :allow_blank => true
|
61
|
+
|
62
|
+
# Must use custom validation since uniqueness scope will not work with has_many association
|
63
|
+
validate :challenge_question_uniqueness
|
64
|
+
validate :challenge_answer_uniqueness
|
65
|
+
validate :challenge_answer_repeating
|
66
|
+
|
67
|
+
before_save :digest_challenge_answer
|
68
|
+
|
69
|
+
def digest_challenge_answer
|
70
|
+
write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer.downcase)) unless self.challenge_answer.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def challenge_question_uniqueness
|
75
|
+
if self.challenge_question.present? && self.{resource}.{resource}_challenge_questions.select{|q| q.challenge_question == self.challenge_question}.count > 1
|
76
|
+
errors.add(:challenge_question, 'can only be used once')
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def challenge_answer_uniqueness
|
81
|
+
if self.challenge_answer.present? && self.{resource}.{resource}_challenge_questions.select{|q| q.challenge_answer == self.challenge_answer}.count > 1
|
82
|
+
errors.add(:challenge_answer, 'can only be used once')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def challenge_answer_repeating
|
87
|
+
if self.challenge_answer.present? && self.challenge_answer =~ /(.)\1{2,}/
|
88
|
+
errors.add(:challenge_answer, 'can not have more then two repeating characters in a row')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
Locales:
|
95
|
+
|
96
|
+
```yaml
|
97
|
+
en:
|
98
|
+
activerecord:
|
99
|
+
attributes:
|
100
|
+
user:
|
101
|
+
user_challenge_questions:
|
102
|
+
challenge_question: "Question"
|
103
|
+
challenge_answer: "Answer"
|
104
|
+
devise:
|
105
|
+
challenge_questions:
|
106
|
+
attempt_failed: "Attempt failed."
|
107
|
+
updated_challenge_questions: "Successfully updated challenge questions."
|
108
|
+
send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
|
109
|
+
```
|
110
|
+
|
111
|
+
Configuration settings
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
# ==> Configuration for :challenge_questionable
|
115
|
+
# Max challenge question attempts
|
116
|
+
config.max_challenge_question_attempts = 3
|
117
|
+
|
118
|
+
# Number of challenge questions to store for each {resource}
|
119
|
+
config.number_of_challenge_questions = 3
|
120
|
+
|
121
|
+
# Default challenge question options
|
122
|
+
config.challenge_questions = ["What was your high school mascot?","In which city was your first elementary school?","In which city was your mother born?","What is the name of your favorite movie?","Who is your favorite athlete?","What was your most memorable gift as a child?","What is your favorite cartoon character?","What is the name of your favorite novel?","Name of favorite childhood pet?","What is the name of your elementary school?","What is your youngest child's middle name?","Last Name of your kindergarten teacher?","What is the first name of your grandmother (your father's mother)?","What is your spouse's nickname?","Name of the place where your wedding reception was held?","Name of a college you applied to but did not attend?","What is the first name of the youngest of your siblings?","What is the first name of the eldest of your siblings?","What is your favorite television show?","If you needed a new first name, what would it be?","What is the first name of your youngest child?","When is your mother's birthday (MM/DD)?","What is your eldest child's middle name?","What is the last name of the funniest friend you know?","Name the highest mountain you've been to the top of?","What is the first name of your grandmother (your mother's mother)?","What is the first name of your grandfather (your mother's father)?","What was the first name of your best man/maid of honor?","What was the last name of your first grade teacher?","What is the last name of your first boyfriend or girlfriend?","Which high school did you attend?","What was your major during college?","What was the name of your first pet?","What was your favorite place from your childhood?","What is your favorite song?","What is your favorite car?","What is your mother’s middle name?","What is the (MM/DD) of your employment?","What is the make/model of first car?","What is the name of the city or town where you were born?","What is the name of your favorite childhood teacher?","What is the name of your favorite childhood friend?","What are your oldest sibling’s (MM/YYYY) of birth?","What is your oldest sibling’s middle name?","What school did you attend for sixth grade?","On what street did you live in third grade?"]
|
123
|
+
|
124
|
+
```
|
125
|
+
|
126
|
+
### Customization
|
127
|
+
|
128
|
+
By default challenge questions are enabled for each {resource}, you can change it with this method in your User model:
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
def need_challenge_questions?(request)
|
132
|
+
request.ip != '127.0.0.1'
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
this will disable challenge questions for local {resource}s
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,106 @@
|
|
1
|
+
class Devise::ChallengeQuestionsController < ApplicationController
|
2
|
+
include Devise::Controllers::InternalHelpers
|
3
|
+
prepend_before_filter :require_no_authentication, :only => [ :new, :create, :edit, :update ]
|
4
|
+
prepend_before_filter :authenticate_scope!, :only => [:show, :authenticate, :manage, :forgot]
|
5
|
+
before_filter :prepare_and_validate, :handle_challenge_questions, :only => [:show, :authenticate]
|
6
|
+
|
7
|
+
# GET /resource/challenge_question/new
|
8
|
+
def new
|
9
|
+
build_resource
|
10
|
+
render_with_scope :new
|
11
|
+
end
|
12
|
+
|
13
|
+
# POST /resource/challenge_question
|
14
|
+
def create
|
15
|
+
self.resource = resource_class.send_reset_challenge_questions_instructions(params[resource_name])
|
16
|
+
|
17
|
+
if resource.errors.empty?
|
18
|
+
set_flash_message :notice, :send_instructions
|
19
|
+
redirect_to new_session_path(resource_name)
|
20
|
+
else
|
21
|
+
render_with_scope :new
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# GET /resource/challenge_question/edit?reset_challenge_questions_token=abcdef
|
26
|
+
def edit
|
27
|
+
self.resource = resource_class.new
|
28
|
+
resource.reset_challenge_questions_token = params[:reset_challenge_questions_token]
|
29
|
+
Devise.number_of_challenge_questions.times { resource.send("#{resource_name}_challenge_questions").build }
|
30
|
+
render_with_scope :edit
|
31
|
+
end
|
32
|
+
|
33
|
+
# PUT /resource/challenge_question
|
34
|
+
def update
|
35
|
+
self.resource = resource_class.reset_challenge_questions_by_token(params[resource_name])
|
36
|
+
|
37
|
+
if resource.errors.empty?
|
38
|
+
set_flash_message :notice, :updated_challenge_questions
|
39
|
+
redirect_to :root
|
40
|
+
else
|
41
|
+
render_with_scope :edit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# GET /resource/challenge_question
|
46
|
+
def show
|
47
|
+
@challenge_question = resource.send("#{resource_name}_challenge_questions").sample
|
48
|
+
if @challenge_question.nil?
|
49
|
+
resource.set_reset_challenge_questions_token
|
50
|
+
sign_out(resource)
|
51
|
+
redirect_to edit_challenge_question_path(resource, :reset_challenge_questions_token => resource.reset_challenge_questions_token)
|
52
|
+
else
|
53
|
+
render_with_scope :show
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def authenticate
|
58
|
+
render_with_scope :show and return if params[:challenge_answer].nil?
|
59
|
+
@challenge_question = resource.send("#{resource_name}_challenge_questions").find(params[:challenge_question_id])
|
60
|
+
md5 = Digest::MD5.hexdigest(params[:challenge_answer].downcase)
|
61
|
+
if md5.eql?(@challenge_question.challenge_answer)
|
62
|
+
warden.session(resource_name)[:need_challenge_questions] = false
|
63
|
+
sign_in resource_name, resource
|
64
|
+
redirect_to stored_location_for(resource_name) || :root
|
65
|
+
resource.update_attribute(:challenge_question_failed_attempts, 0)
|
66
|
+
else
|
67
|
+
resource.challenge_question_failed_attempts += 1
|
68
|
+
resource.save
|
69
|
+
set_flash_message :notice, :attempt_failed
|
70
|
+
if resource.max_challenge_question_attempts?
|
71
|
+
resource.max_challenge_question_lock_account
|
72
|
+
sign_out(resource)
|
73
|
+
render_with_scope :max_challenge_question_attempts_reached and return
|
74
|
+
else
|
75
|
+
redirect_to send("#{resource_name}_challenge_question_path")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Build token and redirect
|
81
|
+
def manage
|
82
|
+
resource.set_reset_challenge_questions_token
|
83
|
+
sign_out(resource)
|
84
|
+
redirect_to edit_challenge_question_path(resource, :reset_challenge_questions_token => resource.reset_challenge_questions_token)
|
85
|
+
end
|
86
|
+
|
87
|
+
def forgot
|
88
|
+
sign_out(resource)
|
89
|
+
redirect_to new_challenge_question_path(resource_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def authenticate_scope!
|
95
|
+
self.resource = send("current_#{resource_name}")
|
96
|
+
end
|
97
|
+
|
98
|
+
def prepare_and_validate
|
99
|
+
redirect_to :root and return if resource.nil?
|
100
|
+
@limit = resource.class.max_challenge_question_attempts
|
101
|
+
if resource.max_challenge_question_attempts?
|
102
|
+
sign_out(resource)
|
103
|
+
render_with_scope :max_challenge_question_attempts_reached and return
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
<h3>Change Your Challenge Questions</h3>
|
2
|
+
<p>Change your current challenge questions by filling out the form below. The questions you set up previously will be replaced with these new questions. All fields are required.</p>
|
3
|
+
|
4
|
+
<fieldset>
|
5
|
+
<h4>Please select <%= Devise.number_of_challenge_questions %> questions below, 1 from each drop down. Then type your answers in the field provided.</h4>
|
6
|
+
<%= form_for(resource, :as => resource_name, :url => challenge_question_path(resource_name), :html => { :method => :put }) do |f| %>
|
7
|
+
<%= devise_error_messages! %>
|
8
|
+
<%= f.hidden_field :id %>
|
9
|
+
<%= f.hidden_field :reset_challenge_questions_token %>
|
10
|
+
|
11
|
+
<%= f.fields_for :"#{resource_name}_challenge_questions", resource.send("#{resource_name}_challenge_questions") do |cq| %>
|
12
|
+
<% if cq.object.new_record? %>
|
13
|
+
<p>
|
14
|
+
Question: <%= cq.select :challenge_question, Devise.challenge_questions.shuffle, :prompt => 'Please select a question' %>
|
15
|
+
<br />
|
16
|
+
Answer: <%= cq.text_field :challenge_answer, :placeholder => "Type your answer here", :autocomplete => "off" %>
|
17
|
+
<hr>
|
18
|
+
</p>
|
19
|
+
<% end %>
|
20
|
+
<% end %>
|
21
|
+
<p>Remember, your current Challenge Questions will be overwritten.</p>
|
22
|
+
<p><%= f.submit "Change my challenge questions" %></p>
|
23
|
+
<% end %>
|
24
|
+
</fieldset>
|
25
|
+
|
26
|
+
<%- content_for :sidebar do %>
|
27
|
+
<div>
|
28
|
+
<h3>Tips for Creating Security Questions</h3>
|
29
|
+
<ul>
|
30
|
+
<li>1. Please choose questions with clear answers that you can remember easily.</li>
|
31
|
+
<li>2. All questions must be answered.</li>
|
32
|
+
<li>3. You cannot use the same answer for more than 1 question.</li>
|
33
|
+
<li>4. Answers must have between 3 and 56 characters.</li>
|
34
|
+
<li>5. Answers cannot have more than 2 repeating characters in a row.</li>
|
35
|
+
<li>6. Special characters, such as & and () are not allowed.</li>
|
36
|
+
<li>7. Answers are not case sensitive.</li>
|
37
|
+
</ul>
|
38
|
+
</div>
|
39
|
+
<% end %>
|
40
|
+
|
41
|
+
<script type="text/javascript" language="javascript">
|
42
|
+
if (window.jQuery) {
|
43
|
+
jQuery(document).ready(function() {
|
44
|
+
$("select").change(function () {
|
45
|
+
var $this = $(this);
|
46
|
+
var prevVal = $this.data("prev");
|
47
|
+
var otherSelects = $("select").not(this);
|
48
|
+
otherSelects.find('option:contains('+ $(this).val() +')').attr('disabled', 'disabled');
|
49
|
+
if (prevVal) {
|
50
|
+
otherSelects.find('option:contains('+ prevVal +')').removeAttr('disabled');
|
51
|
+
}
|
52
|
+
$this.data("prev", $this.val());
|
53
|
+
});
|
54
|
+
|
55
|
+
$("select").each(function () {
|
56
|
+
var $this = $(this);
|
57
|
+
var prevVal = $this.data("prev");
|
58
|
+
var otherSelects = $("select").not(this);
|
59
|
+
otherSelects.find('option:contains('+ $(this).val() +')').attr('disabled', 'disabled');
|
60
|
+
$this.data("prev", $this.val());
|
61
|
+
});
|
62
|
+
});
|
63
|
+
}
|
64
|
+
</script>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<h3>Please contact your system administrator.</h3>
|
2
|
+
|
3
|
+
<fieldset>
|
4
|
+
<p>
|
5
|
+
Access denied as you have reached your max challenge question attempts.
|
6
|
+
</p>
|
7
|
+
<p>
|
8
|
+
<%= link_to "Forgot your challenge questions?", new_challenge_question_path(resource_name) %>
|
9
|
+
</p>
|
10
|
+
</fieldset>
|
11
|
+
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<h3>Forgot your challenge questions?</h3>
|
2
|
+
|
3
|
+
<%= form_for(resource, :as => resource_name, :url => challenge_question_path(resource_name), :html => { :method => :post }) do |f| %>
|
4
|
+
<fieldset>
|
5
|
+
<%= devise_error_messages! %>
|
6
|
+
|
7
|
+
<p><%= f.label :email %><br />
|
8
|
+
<%= f.text_field :email, :autocomplete => "off", :placeholder => "example@email.com" %></p>
|
9
|
+
|
10
|
+
<p><%= f.submit "Send me reset challenge question instructions" %></p>
|
11
|
+
</fieldset>
|
12
|
+
<% end %>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<h3>Answer the challenge question below</h3>
|
2
|
+
|
3
|
+
<%= form_tag([:authenticate, resource_name, :challenge_question], :method => :put) do %>
|
4
|
+
<fieldset>
|
5
|
+
<p>
|
6
|
+
<%= hidden_field_tag :challenge_question_id, @challenge_question.id%>
|
7
|
+
<%= label_tag :challege_question, @challenge_question.challenge_question %>
|
8
|
+
<br>
|
9
|
+
<%= password_field_tag :challenge_answer, nil, :placeholder => 'Answer', :class => 'wide', :style => 'font-size: 1.8em;padding:5px', :autofocus => 'true', :autocomplete => "off" %>
|
10
|
+
</p>
|
11
|
+
<p>
|
12
|
+
<%= submit_tag "Submit", :style => 'font-size:1.8em;'%>
|
13
|
+
or
|
14
|
+
<%= link_to "Cancel", destroy_user_session_path %>
|
15
|
+
</p>
|
16
|
+
<p>
|
17
|
+
<%= link_to "Forgot your challenge questions?", forgot_challenge_question_path(resource_name) %>
|
18
|
+
</p>
|
19
|
+
</fieldset>
|
20
|
+
<% end %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<p>Hello <%= @resource.email %>!</p>
|
2
|
+
|
3
|
+
<p>Someone has requested a link to change your challenge questions, and you can do this through the link below.</p>
|
4
|
+
|
5
|
+
<p><%= link_to 'Change my challenge questions', edit_challenge_question_url(@resource, :reset_challenge_questions_token => @resource.reset_challenge_questions_token) %></p>
|
6
|
+
|
7
|
+
<p>If you didn't request this, please ignore this email.</p>
|
8
|
+
<p>Your challenge questions won't change until you access the link above and create a new ones.</p>
|
@@ -0,0 +1,13 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
attributes:
|
4
|
+
user:
|
5
|
+
user_challenge_questions:
|
6
|
+
challenge_question: "Question"
|
7
|
+
challenge_answer: "Answer"
|
8
|
+
devise:
|
9
|
+
challenge_questions:
|
10
|
+
attempt_failed: "Attempt failed."
|
11
|
+
updated_challenge_questions: "Successfully updated challenge questions."
|
12
|
+
send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
|
13
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "devise_challenge_questionable/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "devise_challenge_questionable"
|
7
|
+
s.version = DeviseChallengeQuestionable::VERSION.dup
|
8
|
+
s.authors = ["Andrew Kennedy"]
|
9
|
+
s.email = ["andrew@akennedy.io"]
|
10
|
+
s.homepage = "https://github.com/akennedy/devise_challenge_questionable"
|
11
|
+
s.summary = %q{Challenge question plugin for devise}
|
12
|
+
s.description = <<-EOF
|
13
|
+
### Features ###
|
14
|
+
* configure max challenge question attempts
|
15
|
+
* per user level control if he really need challenge questions
|
16
|
+
EOF
|
17
|
+
|
18
|
+
s.rubyforge_project = "devise_challenge_questionable"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
s.add_runtime_dependency 'rails', '3.0.20'
|
26
|
+
s.add_runtime_dependency 'devise', '1.1.3'
|
27
|
+
|
28
|
+
s.add_development_dependency 'bundler'
|
29
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'devise_challenge_questionable/version'
|
2
|
+
require 'devise'
|
3
|
+
require 'digest'
|
4
|
+
require 'active_support/concern'
|
5
|
+
|
6
|
+
module Devise
|
7
|
+
mattr_accessor :max_challenge_question_attempts
|
8
|
+
@@max_challenge_question_attempts = 3
|
9
|
+
|
10
|
+
mattr_accessor :challenge_questions
|
11
|
+
@@challenge_questions = ["What was your high school mascot?","In which city was your first elementary school?","In which city was your mother born?","What is the name of your favorite movie?","Who is your favorite athlete?","What was your most memorable gift as a child?","What is your favorite cartoon character?","What is the name of your favorite novel?","Name of favorite childhood pet?","What is the name of your elementary school?","What is your youngest child's middle name?","Last Name of your kindergarten teacher?","What is the first name of your grandmother (your father's mother)?","What is your spouse's nickname?","Name of the place where your wedding reception was held?","Name of a college you applied to but did not attend?","What is the first name of the youngest of your siblings?","What is the first name of the eldest of your siblings?","What is your favorite television show?","If you needed a new first name, what would it be?","What is the first name of your youngest child?","When is your mother's birthday (MM/DD)?","What is your eldest child's middle name?","What is the last name of the funniest friend you know?","Name the highest mountain you've been to the top of?","What is the first name of your grandmother (your mother's mother)?","What is the first name of your grandfather (your mother's father)?","What was the first name of your best man/maid of honor?","What was the last name of your first grade teacher?","What is the last name of your first boyfriend or girlfriend?","Which high school did you attend?","What was your major during college?","What was the name of your first pet?","What was your favorite place from your childhood?","What is your favorite song?","What is your favorite car?","What is your mother’s middle name?","What is the (MM/DD) of your employment?","What is the make/model of first car?","What is the name of the city or town where you were born?","What is the name of your favorite childhood teacher?","What is the name of your favorite childhood friend?","What are your oldest sibling’s (MM/YYYY) of birth?","What is your oldest sibling’s middle name?","What school did you attend for sixth grade?","On what street did you live in third grade?"]
|
12
|
+
|
13
|
+
|
14
|
+
mattr_accessor :number_of_challenge_questions
|
15
|
+
@@number_of_challenge_questions = 3
|
16
|
+
end
|
17
|
+
|
18
|
+
module DeviseChallengeQuestionable
|
19
|
+
autoload :Schema, 'devise_challenge_questionable/schema'
|
20
|
+
module Controllers
|
21
|
+
autoload :Helpers, 'devise_challenge_questionable/controllers/helpers'
|
22
|
+
autoload :UrlHelpers, 'devise_challenge_questionable/controllers/url_helpers'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Devise.add_module :challenge_questionable, :model => 'devise_challenge_questionable/model', :controller => :challenge_questions, :route => :challenge_question
|
27
|
+
|
28
|
+
require 'devise_challenge_questionable/mailer'
|
29
|
+
require 'devise_challenge_questionable/routes'
|
30
|
+
require 'devise_challenge_questionable/model'
|
31
|
+
require 'devise_challenge_questionable/rails'
|
32
|
+
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module DeviseChallengeQuestionable
|
2
|
+
module Controllers
|
3
|
+
module Helpers
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
before_filter :handle_challenge_questions
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def handle_challenge_questions
|
13
|
+
unless devise_controller? && (controller_name != 'registrations' && ![:edit, :update, :destroy].include?(action_name))
|
14
|
+
Devise.mappings.keys.flatten.any? do |scope|
|
15
|
+
if signed_in?(scope) and warden.session(scope)[:need_challenge_questions]
|
16
|
+
failed_challenge_question(scope)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def failed_challenge_question(scope)
|
23
|
+
if request.format.present? and request.format.html?
|
24
|
+
session["#{scope}_return_to"] = request.path if request.get?
|
25
|
+
redirect_to challenge_questions_path_for(scope)
|
26
|
+
else
|
27
|
+
render :nothing => true, :status => :unauthorized
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def challenge_questions_path_for(resource_or_scope = nil)
|
32
|
+
scope = Devise::Mapping.find_scope!(resource_or_scope)
|
33
|
+
change_path = "#{scope}_challenge_question_path"
|
34
|
+
send(change_path)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DeviseChallengeQuestionable
|
2
|
+
module Controllers
|
3
|
+
module UrlHelpers
|
4
|
+
[:path, :url].each do |path_or_url|
|
5
|
+
[nil, :edit_, :authenticate_, :manage_, :forgot_].each do |action|
|
6
|
+
class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
|
7
|
+
def #{action}challenge_question_#{path_or_url}(resource, *args)
|
8
|
+
resource = case resource
|
9
|
+
when Symbol, String
|
10
|
+
resource
|
11
|
+
when Class
|
12
|
+
resource.name.underscore
|
13
|
+
else
|
14
|
+
resource.class.name.underscore
|
15
|
+
end
|
16
|
+
|
17
|
+
send("#{action}\#{resource}_challenge_question_#{path_or_url}", *args)
|
18
|
+
end
|
19
|
+
URL_HELPERS
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'devise_challenge_questionable/hooks/challenge_questions'
|
2
|
+
module Devise
|
3
|
+
module Models
|
4
|
+
module ChallengeQuestionable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Update challenge questions saving the record and clearing token. Returns true if
|
8
|
+
# the challenge questions are valid and the record was saved, false otherwise.
|
9
|
+
def reset_challenge_questions!(attributes)
|
10
|
+
self.send("#{self.class.name.underscore}_challenge_questions").destroy_all
|
11
|
+
self.attributes = {"#{self.class.name.underscore}_challenge_questions_attributes" => attributes["#{self.class.name.underscore}_challenge_questions_attributes"]}
|
12
|
+
clear_reset_challenge_questions_token if self.valid?
|
13
|
+
self.save
|
14
|
+
end
|
15
|
+
|
16
|
+
def need_challenge_questions?(request)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def max_challenge_question_lock_account
|
21
|
+
self.lock_access! if self.class.lock_strategy_enabled?(:failed_attempts)
|
22
|
+
end
|
23
|
+
|
24
|
+
def max_challenge_question_attempts?
|
25
|
+
challenge_question_failed_attempts >= self.class.max_challenge_question_attempts
|
26
|
+
end
|
27
|
+
|
28
|
+
# Resets reset challenge question token and send reset challenge question instructions by email
|
29
|
+
def send_reset_challenge_questions_instructions
|
30
|
+
generate_reset_challenge_questions_token!
|
31
|
+
::Devise.mailer.reset_challenge_questions_instructions(self).deliver
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a new random token for reset challenge_question
|
35
|
+
def set_reset_challenge_questions_token
|
36
|
+
generate_reset_challenge_questions_token!
|
37
|
+
end
|
38
|
+
|
39
|
+
def unlock_access_including_challenge_questions!
|
40
|
+
if_access_locked do
|
41
|
+
self.locked_at = nil
|
42
|
+
self.failed_attempts = 0 if respond_to?(:failed_attempts=)
|
43
|
+
self.challenge_question_failed_attempts = 0 if respond_to?(:challenge_question_failed_attempts=)
|
44
|
+
self.unlock_token = nil if respond_to?(:unlock_token=)
|
45
|
+
save(:validate => false)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
# Generates a new random token for reset challenge_question
|
52
|
+
def generate_reset_challenge_questions_token
|
53
|
+
self.reset_challenge_questions_token = self.class.reset_challenge_questions_token
|
54
|
+
end
|
55
|
+
|
56
|
+
# Resets the reset challenge_question token with and save the record without
|
57
|
+
# validating
|
58
|
+
def generate_reset_challenge_questions_token!
|
59
|
+
generate_reset_challenge_questions_token && save(:validate => false)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Removes reset_challenge_question token
|
63
|
+
def clear_reset_challenge_questions_token
|
64
|
+
self.reset_challenge_questions_token = nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def clear_challenge_question_failed_attempts
|
68
|
+
self.challenge_question_failed_attempts = 0
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
::Devise::Models.config(self, :max_challenge_question_attempts)
|
73
|
+
|
74
|
+
# Attempt to find a user by it's email. If a record is found, send new
|
75
|
+
# challenge_question instructions to it. If not user is found, returns a new user
|
76
|
+
# with an email not found error.
|
77
|
+
# Attributes must contain the user email
|
78
|
+
def send_reset_challenge_questions_instructions(attributes={})
|
79
|
+
challenge_questionable = find_or_initialize_with_error_by(:email, attributes[:email], :not_found)
|
80
|
+
challenge_questionable.send_reset_challenge_questions_instructions if challenge_questionable.persisted?
|
81
|
+
challenge_questionable
|
82
|
+
end
|
83
|
+
|
84
|
+
# Generate a token checking if one does not already exist in the database.
|
85
|
+
def reset_challenge_questions_token
|
86
|
+
generate_token(:reset_challenge_questions_token)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Attempt to find a user by it's reset_challenge_questions_token to reset it's
|
90
|
+
# challenge_question. If a user is found, reset it's challenge_question and automatically
|
91
|
+
# try saving the record. If not user is found, returns a new user
|
92
|
+
# containing an error in reset_challenge_questions_token attribute.
|
93
|
+
# Attributes must contain reset_challenge_questions_token, challenge_question and confirmation
|
94
|
+
def reset_challenge_questions_by_token(attributes={})
|
95
|
+
challenge_questionable = find_or_initialize_with_error_by(:reset_challenge_questions_token, attributes[:reset_challenge_questions_token])
|
96
|
+
challenge_questionable.reset_challenge_questions!(attributes) if challenge_questionable.persisted?
|
97
|
+
challenge_questionable
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module DeviseChallengeQuestionable
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
ActiveSupport.on_load(:action_controller) { include DeviseChallengeQuestionable::Controllers::Helpers }
|
4
|
+
ActiveSupport.on_load(:action_controller) { include DeviseChallengeQuestionable::Controllers::UrlHelpers }
|
5
|
+
ActiveSupport.on_load(:action_view) { include DeviseChallengeQuestionable::Controllers::UrlHelpers }
|
6
|
+
|
7
|
+
# We use to_prepare instead of after_initialize here because Devise is a Rails engine; its
|
8
|
+
# mailer is reloaded like the rest of the user's app. Got to make sure that our mailer methods
|
9
|
+
# are included each time Devise::Mailer is (re)loaded.
|
10
|
+
config.to_prepare do
|
11
|
+
require 'devise/mailer'
|
12
|
+
Devise::Mailer.send :include, DeviseChallengeQuestionable::Mailer
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
initializer "devise.add_filters" do |app|
|
17
|
+
app.config.filter_parameters += [:challenge_answer, :reset_challenge_questions_token]
|
18
|
+
app.config.filter_parameters.uniq
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActionDispatch::Routing
|
2
|
+
class Mapper
|
3
|
+
protected
|
4
|
+
|
5
|
+
def devise_challenge_question(mapping, controllers)
|
6
|
+
resource :challenge_question, :only => [:show, :new, :create, :edit, :update], :path => mapping.path_names[:challenge_question], :controller => controllers[:challenge_questions] do
|
7
|
+
put :authenticate, :path => mapping.path_names[:authenticate]
|
8
|
+
get :manage, :path => mapping.path_names[:manage]
|
9
|
+
get :forgot, :path => mapping.path_names[:forgot]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'rails/generators/active_record'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Generators
|
5
|
+
class DeviseChallengeQuestionableGenerator < ActiveRecord::Generators::Base
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def copy_migration
|
9
|
+
migration_template "migration.rb", "db/migrate/devise_challenge_questionable_add_to_#{table_name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class DeviseChallengeQuestionableAddTo<%= table_name.camelize %> < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
add_column :<%= table_name %>, :reset_challenge_questions_token, :string
|
4
|
+
add_column :<%= table_name %>, :challenge_question_failed_attempts, :integer, :default => 0
|
5
|
+
|
6
|
+
create_table :<%= class_name.underscore %>_challenge_questions do |t|
|
7
|
+
t.integer :<%= table_name.singularize %>_id
|
8
|
+
t.string :challenge_question
|
9
|
+
t.string :challenge_answer
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :<%= class_name.underscore %>_challenge_questions
|
17
|
+
remove_column :<%= table_name %>, :reset_challenge_questions_token
|
18
|
+
remove_column :<%= table_name %>, :challenge_question_failed_attempts
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module DeviseChallengeQuestionable
|
2
|
+
module Generators
|
3
|
+
class DeviseChallengeQuestionableGenerator < Rails::Generators::NamedBase
|
4
|
+
source_root File.expand_path("../templates", __FILE__)
|
5
|
+
namespace "devise_challenge_questionable"
|
6
|
+
|
7
|
+
desc "Adds :challenge_questionable directive in the given model. It also generates an active record migration."
|
8
|
+
|
9
|
+
def inject_devise_challenge_questionable_content
|
10
|
+
path = File.join("app", "models", "#{file_path}.rb")
|
11
|
+
inject_into_file(path, "challenge_questionable, :", :after => "devise :") if File.exists?(path)
|
12
|
+
inject_into_file(path, "user_challenge_questions_attributes, :", :after => "attr_accessible :") if File.exists?(path)
|
13
|
+
inject_into_file path, :after => "class #{class_name} < ActiveRecord::Base\n" do
|
14
|
+
<<-CONTENT
|
15
|
+
has_many :#{class_name.underscore}_challenge_questions, :validate => true, :inverse_of => :user
|
16
|
+
accepts_nested_attributes_for :#{class_name.underscore}_challenge_questions, :allow_destroy => true
|
17
|
+
CONTENT
|
18
|
+
end if File.exists?(path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_model
|
22
|
+
invoke "active_record:model", ["#{file_path}_challenge_question"], :migration => false
|
23
|
+
end
|
24
|
+
|
25
|
+
def inject_scope_challenge_question_content
|
26
|
+
path = File.join("app", "models", "#{file_path}_challenge_question.rb")
|
27
|
+
inject_into_file path, :before => /^end/ do
|
28
|
+
<<-CONTENT
|
29
|
+
belongs_to :#{class_name.underscore}
|
30
|
+
|
31
|
+
validates :challenge_question, :challenge_answer, :presence => true
|
32
|
+
validates :challenge_answer, :length => { :in => 3..56 }, :format => {:with => /^[\\w\\s:]*$/, :message => "can not contain special characters"}, :allow_blank => true
|
33
|
+
|
34
|
+
# Must use custom validation since uniqueness scope will not work with has_many association
|
35
|
+
validate :challenge_question_uniqueness
|
36
|
+
validate :challenge_answer_uniqueness
|
37
|
+
validate :challenge_answer_repeating
|
38
|
+
|
39
|
+
before_save :digest_challenge_answer
|
40
|
+
|
41
|
+
def digest_challenge_answer
|
42
|
+
write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer.downcase)) unless self.challenge_answer.nil?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def challenge_question_uniqueness
|
47
|
+
if self.challenge_question.present? && self.#{class_name.underscore}.#{class_name.underscore}_challenge_questions.select{|q| q.challenge_question == self.challenge_question}.count > 1
|
48
|
+
errors.add(:challenge_question, 'can only be used once')
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def challenge_answer_uniqueness
|
53
|
+
if self.challenge_answer.present? && self.#{class_name.underscore}.#{class_name.underscore}_challenge_questions.select{|q| q.challenge_answer == self.challenge_answer}.count > 1
|
54
|
+
errors.add(:challenge_answer, 'can only be used once')
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def challenge_answer_repeating
|
59
|
+
if self.challenge_answer.present? && self.challenge_answer =~ /(.)\1{2,}/
|
60
|
+
errors.add(:challenge_answer, 'can not have more then two repeating characters in a row')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
CONTENT
|
64
|
+
end if File.exists?(path)
|
65
|
+
end
|
66
|
+
|
67
|
+
def inject_localization_model_content
|
68
|
+
path = File.join("config", "locales", "devise.en.yml")
|
69
|
+
inject_into_file path, :after => "en:\n" do
|
70
|
+
<<-CONTENT
|
71
|
+
activerecord:
|
72
|
+
attributes:
|
73
|
+
#{class_name.underscore}:
|
74
|
+
#{class_name.underscore}_challenge_questions:
|
75
|
+
challenge_question: "Question"
|
76
|
+
challenge_answer: "Answer"
|
77
|
+
CONTENT
|
78
|
+
end if File.exists?(path)
|
79
|
+
inject_into_file path, :after => "devise:\n" do
|
80
|
+
<<-CONTENT
|
81
|
+
challenge_questions:
|
82
|
+
attempt_failed: "Attempt failed."
|
83
|
+
updated_challenge_questions: "Successfully updated challenge questions."
|
84
|
+
send_instructions: "You will receive an email with instructions about how to reset your challenge questions in a few minutes."
|
85
|
+
CONTENT
|
86
|
+
end if File.exists?(path)
|
87
|
+
end
|
88
|
+
|
89
|
+
hook_for :orm
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module DeviseChallengeQuestionable
|
2
|
+
module Generators
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path("../../templates", __FILE__)
|
5
|
+
desc "Add DeviseChallengeQuestionable config variables to the Devise initializer and copy DeviseChallengeQuestionable locale files to your application."
|
6
|
+
|
7
|
+
def add_config_options_to_initializer
|
8
|
+
devise_initializer_path = "config/initializers/devise.rb"
|
9
|
+
if File.exist?(devise_initializer_path)
|
10
|
+
old_content = File.read(devise_initializer_path)
|
11
|
+
|
12
|
+
if old_content.match(Regexp.new(/^\s# ==> Configuration for :challenge_questionable\n/))
|
13
|
+
false
|
14
|
+
else
|
15
|
+
inject_into_file(devise_initializer_path, :before => " # ==> Configuration for :confirmable\n") do
|
16
|
+
<<-CONTENT
|
17
|
+
# ==> Configuration for :challenge_questionable
|
18
|
+
# Max challenge question attempts
|
19
|
+
config.max_challenge_question_attempts = 3
|
20
|
+
|
21
|
+
# Number of challenge questions to store for each user
|
22
|
+
config.number_of_challenge_questions = 3
|
23
|
+
|
24
|
+
# Default challenge question options
|
25
|
+
config.challenge_questions = ["What was your high school mascot?","In which city was your first elementary school?","In which city was your mother born?","What is the name of your favorite movie?","Who is your favorite athlete?","What was your most memorable gift as a child?","What is your favorite cartoon character?","What is the name of your favorite novel?","Name of favorite childhood pet?","What is the name of your elementary school?","What is your youngest child's middle name?","Last Name of your kindergarten teacher?","What is the first name of your grandmother (your father's mother)?","What is your spouse's nickname?","Name of the place where your wedding reception was held?","Name of a college you applied to but did not attend?","What is the first name of the youngest of your siblings?","What is the first name of the eldest of your siblings?","What is your favorite television show?","If you needed a new first name, what would it be?","What is the first name of your youngest child?","When is your mother's birthday (MM/DD)?","What is your eldest child's middle name?","What is the last name of the funniest friend you know?","Name the highest mountain you've been to the top of?","What is the first name of your grandmother (your mother's mother)?","What is the first name of your grandfather (your mother's father)?","What was the first name of your best man/maid of honor?","What was the last name of your first grade teacher?","What is the last name of your first boyfriend or girlfriend?","Which high school did you attend?","What was your major during college?","What was the name of your first pet?","What was your favorite place from your childhood?","What is your favorite song?","What is your favorite car?","What is your mother’s middle name?","What is the (MM/DD) of your employment?","What is the make/model of first car?","What is the name of the city or town where you were born?","What is the name of your favorite childhood teacher?","What is the name of your favorite childhood friend?","What are your oldest sibling’s (MM/YYYY) of birth?","What is your oldest sibling’s middle name?","What school did you attend for sixth grade?","On what street did you live in third grade?"]
|
26
|
+
|
27
|
+
CONTENT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class <%= class_name %>ChallengeQuestion < ActiveRecord::Base
|
2
|
+
|
3
|
+
belongs_to :<%= class_name.underscore %>
|
4
|
+
|
5
|
+
validates :challenge_question, :uniqueness => {:scope => :<%= class_name.underscore %>_id}
|
6
|
+
validates :challenge_answer, :presence => true
|
7
|
+
|
8
|
+
before_save :digest_challenge_answer
|
9
|
+
|
10
|
+
def digest_challenge_answer
|
11
|
+
write_attribute(:challenge_answer, Digest::MD5.hexdigest(self.challenge_answer)) unless self.challenge_answer.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'generators/devise/views_generator'
|
2
|
+
|
3
|
+
module DeviseChallengeQuestionable
|
4
|
+
module Generators
|
5
|
+
class ViewsGenerator < Devise::Generators::ViewsGenerator
|
6
|
+
source_root File.expand_path("../../../../app/views", __FILE__)
|
7
|
+
desc 'Copies all DeviseChallengeQuestionable views to your application.'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: devise_challenge_questionable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andrew Kennedy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2014-01-23 00:00:00 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
prerelease: false
|
17
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - "="
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.20
|
22
|
+
type: :runtime
|
23
|
+
version_requirements: *id001
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: devise
|
26
|
+
prerelease: false
|
27
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - "="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.1.3
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id002
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: bundler
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- &id004
|
40
|
+
- ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: "0"
|
43
|
+
type: :development
|
44
|
+
version_requirements: *id003
|
45
|
+
description: " ### Features ###\n * configure max challenge question attempts\n * per user level control if he really need challenge questions\n"
|
46
|
+
email:
|
47
|
+
- andrew@akennedy.io
|
48
|
+
executables: []
|
49
|
+
|
50
|
+
extensions: []
|
51
|
+
|
52
|
+
extra_rdoc_files: []
|
53
|
+
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- Gemfile
|
57
|
+
- LICENSE
|
58
|
+
- README.md
|
59
|
+
- Rakefile
|
60
|
+
- app/controllers/devise/challenge_questions_controller.rb
|
61
|
+
- app/views/devise/challenge_questions/edit.html.erb
|
62
|
+
- app/views/devise/challenge_questions/max_challenge_question_attempts_reached.html.erb
|
63
|
+
- app/views/devise/challenge_questions/new.html.erb
|
64
|
+
- app/views/devise/challenge_questions/show.html.erb
|
65
|
+
- app/views/devise/mailer/reset_challenge_questions_instructions.html.erb
|
66
|
+
- config/locales/en.yml
|
67
|
+
- devise_challenge_questionable.gemspec
|
68
|
+
- lib/devise_challenge_questionable.rb
|
69
|
+
- lib/devise_challenge_questionable/controllers/helpers.rb
|
70
|
+
- lib/devise_challenge_questionable/controllers/url_helpers.rb
|
71
|
+
- lib/devise_challenge_questionable/hooks/challenge_questions.rb
|
72
|
+
- lib/devise_challenge_questionable/mailer.rb
|
73
|
+
- lib/devise_challenge_questionable/model.rb
|
74
|
+
- lib/devise_challenge_questionable/rails.rb
|
75
|
+
- lib/devise_challenge_questionable/routes.rb
|
76
|
+
- lib/devise_challenge_questionable/schema.rb
|
77
|
+
- lib/devise_challenge_questionable/version.rb
|
78
|
+
- lib/generators/active_record/devise_challenge_questionable_generator.rb
|
79
|
+
- lib/generators/active_record/templates/migration.rb
|
80
|
+
- lib/generators/devise_challenge_questionable/devise_challenge_questionable_generator.rb
|
81
|
+
- lib/generators/devise_challenge_questionable/install_generator.rb
|
82
|
+
- lib/generators/devise_challenge_questionable/templates/challenge_question_model.rb
|
83
|
+
- lib/generators/devise_challenge_questionable/views_generator.rb
|
84
|
+
homepage: https://github.com/akennedy/devise_challenge_questionable
|
85
|
+
licenses: []
|
86
|
+
|
87
|
+
metadata: {}
|
88
|
+
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- *id004
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- *id004
|
100
|
+
requirements: []
|
101
|
+
|
102
|
+
rubyforge_project: devise_challenge_questionable
|
103
|
+
rubygems_version: 2.0.13
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: Challenge question plugin for devise
|
107
|
+
test_files: []
|
108
|
+
|