protected 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +151 -0
  3. data/Rakefile +32 -0
  4. data/app/assets/images/protected/Sorting icons.psd +0 -0
  5. data/app/assets/images/protected/back_disabled.png +0 -0
  6. data/app/assets/images/protected/back_enabled.png +0 -0
  7. data/app/assets/images/protected/back_enabled_hover.png +0 -0
  8. data/app/assets/images/protected/favicon.ico +0 -0
  9. data/app/assets/images/protected/forward_disabled.png +0 -0
  10. data/app/assets/images/protected/forward_enabled.png +0 -0
  11. data/app/assets/images/protected/forward_enabled_hover.png +0 -0
  12. data/app/assets/images/protected/glyphicons-halflings-white.png +0 -0
  13. data/app/assets/images/protected/glyphicons-halflings.png +0 -0
  14. data/app/assets/images/protected/sort_asc.png +0 -0
  15. data/app/assets/images/protected/sort_asc_disabled.png +0 -0
  16. data/app/assets/images/protected/sort_both.png +0 -0
  17. data/app/assets/images/protected/sort_desc.png +0 -0
  18. data/app/assets/images/protected/sort_desc_disabled.png +0 -0
  19. data/app/assets/javascripts/protected/application.js +164 -0
  20. data/app/assets/javascripts/protected/bootstrap.min.js +6 -0
  21. data/app/assets/javascripts/protected/jquery.dataTables.min.js +154 -0
  22. data/app/assets/stylesheets/protected/application.css +51 -0
  23. data/app/assets/stylesheets/protected/bootstrap-responsive.min.css +12 -0
  24. data/app/assets/stylesheets/protected/bootstrap.min.css +689 -0
  25. data/app/controllers/protected/admin/protected_controller.rb +14 -0
  26. data/app/controllers/protected/admin/users_controller.rb +70 -0
  27. data/app/controllers/protected/application_controller.rb +18 -0
  28. data/app/controllers/protected/passwords_controller.rb +39 -0
  29. data/app/controllers/protected/sessions_controller.rb +44 -0
  30. data/app/controllers/protected/users_controller.rb +12 -0
  31. data/app/helpers/protected/application_helper.rb +4 -0
  32. data/app/helpers/protected/passwords_helper.rb +21 -0
  33. data/app/mailers/user_mailer.rb +9 -0
  34. data/app/models/protected.rb +5 -0
  35. data/app/models/protected/old_password.rb +18 -0
  36. data/app/models/protected/user.rb +165 -0
  37. data/app/views/layouts/protected/application.html.haml +38 -0
  38. data/app/views/protected/admin/users/_form.html.haml +27 -0
  39. data/app/views/protected/admin/users/_password_fields.html.haml +9 -0
  40. data/app/views/protected/admin/users/_role_fields.html.haml +10 -0
  41. data/app/views/protected/admin/users/confirm_delete.html.haml +9 -0
  42. data/app/views/protected/admin/users/edit.html.haml +9 -0
  43. data/app/views/protected/admin/users/index.html.haml +27 -0
  44. data/app/views/protected/admin/users/new.html.haml +9 -0
  45. data/app/views/protected/admin/users/show.html.haml +0 -0
  46. data/app/views/protected/passwords/edit.html.haml +28 -0
  47. data/app/views/protected/passwords/forgot_password.html.haml +20 -0
  48. data/app/views/protected/passwords/new.html.haml +22 -0
  49. data/app/views/protected/sessions/first_login.html.haml +25 -0
  50. data/app/views/protected/sessions/new.html.haml +22 -0
  51. data/app/views/protected/sessions/not_authorized.html.haml +9 -0
  52. data/app/views/protected/users/_form.html.haml +19 -0
  53. data/app/views/protected/users/edit.html.haml +19 -0
  54. data/app/views/user_mailer/welcome_existing_user.haml +5 -0
  55. data/app/views/user_mailer/welcome_login_instructions.html.haml +6 -0
  56. data/app/views/user_mailer/welcome_password_instructions.haml +7 -0
  57. data/config/initializers/devise.rb +220 -0
  58. data/config/locales/devise.en.yml +62 -0
  59. data/config/routes.rb +34 -0
  60. data/db/migrate/20120418194256_devise_create_add_users.rb +60 -0
  61. data/lib/generators/protected/USAGE +5 -0
  62. data/lib/generators/protected/install/install_generator.rb +26 -0
  63. data/lib/generators/protected/templates/README +32 -0
  64. data/lib/generators/protected/templates/devise.rb +233 -0
  65. data/lib/generators/protected/views/views_generator.rb +40 -0
  66. data/lib/protected.rb +4 -0
  67. data/lib/protected/devise_recoverable_extensions.rb +19 -0
  68. data/lib/protected/engine.rb +10 -0
  69. data/lib/protected/password_utils.rb +46 -0
  70. data/lib/protected/version.rb +3 -0
  71. data/lib/tasks/protected_tasks.rake +44 -0
  72. metadata +414 -0
@@ -0,0 +1,14 @@
1
+ module Protected
2
+ module Admin
3
+ class ProtectedController < Protected::ApplicationController
4
+ before_filter :admin_only
5
+
6
+ protected
7
+ def admin_only
8
+ if !current_user.is_admin?
9
+ redirect_to not_authorized_url
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ module Protected
2
+ module Admin
3
+ class UsersController < ProtectedController
4
+ before_filter :find_user, :except => [:index, :new, :create, :new_search, :search]
5
+ def index
6
+ @users = User.paginate(pagination_params)
7
+ end
8
+
9
+ def new
10
+ @user = User.new
11
+ @user.is_admin = false
12
+ end
13
+
14
+ def update
15
+ @user.is_admin = params[:user][:is_admin] unless current_user == @user
16
+ params[:user].delete(:is_admin)
17
+ if @user.update_without_password(params[:user])
18
+ flash[:success] = "User #{@user.name} updated successfully."
19
+ redirect_to admin_users_url
20
+ else
21
+ render :action => 'edit'
22
+ end
23
+ end
24
+
25
+ def destroy
26
+ unless @user == current_user
27
+ @user.destroy
28
+ flash[:success] = "The user was deleted"
29
+ redirect_to ['admin','users']
30
+ else
31
+ flash[:error] = "The user could not be deleted"
32
+ redirect_to ['admin',@user]
33
+ end
34
+ end
35
+
36
+ def create
37
+ @user = User.new
38
+ if params[:user].present?
39
+ @user.is_admin = params[:user][:is_admin]
40
+ params[:user].delete(:is_admin)
41
+ end
42
+ @user.attributes = params[:user]
43
+ @user.random_password
44
+ if @user.save
45
+ flash[:success] = "User #{@user.name} created successfully."
46
+ redirect_to ['admin','users']
47
+ else
48
+ render :action => :new
49
+ end
50
+ rescue Errno::ETIMEDOUT
51
+ @user.errors.add(:base, I18n.t('devise.failure.user.service_error'))
52
+ render :action => :new
53
+ end
54
+
55
+ def unlock
56
+ @user.unlock_access!
57
+ flash[:success] = "User's account has been unlocked"
58
+ redirect_to admin_users_url
59
+ end
60
+
61
+ protected
62
+ def find_user
63
+ @user = User.find(params[:id])
64
+ rescue ActiveRecord::RecordNotFound => e
65
+ flash[:error] = e.message
66
+ redirect_to ['admin','users']
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,18 @@
1
+ module Protected
2
+ class ApplicationController < ActionController::Base
3
+ before_filter :authenticate_user!
4
+ before_filter :set_cache_buster
5
+
6
+ protect_from_forgery
7
+
8
+ def pagination_params(opts = {})
9
+ { :page => params[:page].present? ? params[:page].to_i : 1, :per_page => params[:per_page].present? ? params[:per_page].to_i : 12 }.merge(opts)
10
+ end
11
+
12
+ def set_cache_buster
13
+ response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
14
+ response.headers["Pragma"] = "no-cache"
15
+ response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ module Protected
2
+ class PasswordsController < Devise::PasswordsController
3
+ def update
4
+ self.resource = resource_class.reset_password_by_token(params[resource_name])
5
+ if resource.errors.empty?
6
+ flash[:notice] = "Your password has been changed, please log in again."
7
+ sign_out_all_scopes
8
+ redirect_to new_user_session_url and return false
9
+ end
10
+ render :template => 'protected/passwords/edit.html.haml'
11
+ end
12
+
13
+ def new
14
+ build_resource({})
15
+ end
16
+
17
+ def edit
18
+ unless params[:reset_password_token].present?
19
+ flash[:notice] = "A valid password token was not found"
20
+ redirect_to root_url and return false
21
+ else
22
+ self.resource = resource_class.new
23
+ resource.reset_password_token = params[:reset_password_token]
24
+ end
25
+ end
26
+
27
+ def create
28
+
29
+ # Refactor Me:
30
+ # This currently redireccts the user to a success message regardless if the email is in the database or not.
31
+ # This is done to prevent others from determining what emails are "good" within the system but may confuse
32
+ # a user who tries to reset their password but uses an incorrect address. Because they know they have an
33
+ # account and received a success message they will infer the application is broken when no email arrives.
34
+ self.resource = resource_class.reset_password_and_send_password_instructions(params[resource_name])
35
+ flash[:notice] = "Instructions on how to reset your password have been sent to #{resource.email}."
36
+ redirect_to new_user_session_url
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ module Protected
2
+ class SessionsController < Devise::SessionsController
3
+ before_filter :first_login?, :except => [:destroy, :first_login, :update_first_login]
4
+ skip_before_filter :authenticate_user!, :only => [:not_authorized, :new, :create]
5
+
6
+ helper 'protected/application'
7
+
8
+ def first_login
9
+ redirect_to root_url unless current_user
10
+ end
11
+
12
+ def update_first_login
13
+ current_user.update_on_first_login!(params[:user])
14
+ if current_user.errors.any?
15
+ render :action => :first_login
16
+ else
17
+ sign_out_all_scopes
18
+ flash[:notice] = "Your password has been changed, please log in again."
19
+ redirect_to new_user_session_url
20
+ end
21
+ rescue PasswordAlreadyUsedException => e
22
+ current_user.errors.add(:password, e.message)
23
+ render :action => :first_login
24
+ rescue ActiveRecord::RecordInvalid => f
25
+ render :action => :first_login
26
+ end
27
+
28
+ protected
29
+ def first_login?
30
+ if current_user.present? && (current_user.first_login? || params[:wants_first_login] == '1')
31
+ params.delete(:wants_first_login)
32
+ redirect_to first_login_url and return true
33
+ end
34
+ end
35
+
36
+ def after_sign_in_path_for(resource_or_scope)
37
+ if(current_user.is_admin?)
38
+ admin_users_url
39
+ else
40
+ stored_location_for(resource_or_scope) || signed_in_root_path(resource_or_scope)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,12 @@
1
+ module Protected
2
+ class UsersController < ApplicationController
3
+ def update
4
+ if current_user.update_without_password(params[:user])
5
+ flash[:success] = "Your changes have been saved."
6
+ redirect_to current_user
7
+ else
8
+ render :action => :edit
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,4 @@
1
+ module Protected
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,21 @@
1
+ module Protected
2
+ module PasswordsHelper
3
+ MESSAGES = {'match confirmation' => "Passwords should match confirmation",
4
+ '8 characters long' => "Password should be at least 8 characters long",
5
+ 'special character' => "Password should have at least 1 digit, 1 capital letter and 1 special character \(\!\@\#\$\%\^\&\*\(\)\_\-\=\+\)",
6
+ 'previous five passwords' => "You can not use previous five passwords",
7
+ 'password token' => "You must have a valid reset password token" }
8
+
9
+ def password_instructions(messages)
10
+ section = ""
11
+ MESSAGES.each do |error, message|
12
+ section << "<div class=\"alert-message #{ message_class(messages, error) }\">#{message}</div>"
13
+ end
14
+ section.html_safe
15
+ end
16
+
17
+ def message_class(messages, error)
18
+ messages.include?(error) ? "block-message error" : ""
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ class UserMailer <Devise::Mailer
2
+ def welcome_login_instructions(record)
3
+ devise_mail(record, :welcome_login_instructions)
4
+ end
5
+
6
+ def welcome_password_instructions(record)
7
+ devise_mail(record, :welcome_password_instructions)
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module Protected
2
+ def self.table_name_prefix
3
+ 'protected_'
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ # == Schema Information
2
+ #
3
+ # Table name: old_passwords
4
+ #
5
+ # id :integer(4) not null, primary key
6
+ # encrypted_password :string(128) not null
7
+ # password_salt :string(255)
8
+ # password_archivable_id :integer(4) not null
9
+ # password_archivable_type :string(255) not null
10
+ # created_at :datetime
11
+ #
12
+ module Protected
13
+ class OldPassword < ActiveRecord::Base
14
+ belongs_to :password_archivable, :polymorphic => true
15
+
16
+ attr_accessible :encrypted_password, :password_salt, :password_archivable, :password_archivable_type, :password_archivable_id
17
+ end
18
+ end
@@ -0,0 +1,165 @@
1
+
2
+ # == Schema Information
3
+ #
4
+ # Table name: users
5
+ #
6
+ # id :integer(4) not null, primary key
7
+ # first_name :string(255)
8
+ # last_name :string(255)
9
+ # company :string(255)
10
+ # password_salt :string(255) default(""), not null
11
+ # remember_token :string(255)
12
+ # first_login :boolean(1) default(TRUE)
13
+ # is_admin :boolean(1)
14
+ # created_at :datetime
15
+ # updated_at :datetime
16
+ # email :string(255) default(""), not null
17
+ # encrypted_password :string(128) default(""), not null
18
+ # reset_password_token :string(255)
19
+ # reset_password_sent_at :datetime
20
+ # remember_created_at :datetime
21
+ # sign_in_count :integer(4) default(0)
22
+ # current_sign_in_at :datetime
23
+ # last_sign_in_at :datetime
24
+ # current_sign_in_ip :string(255)
25
+ # last_sign_in_ip :string(255)
26
+ # failed_attempts :integer(4) default(0)
27
+ # unlock_token :string(255)
28
+ # locked_at :datetime
29
+ # login :string(255)
30
+ #
31
+ class LoginFormatValidator < ActiveModel::EachValidator
32
+ def validate_each(object, attribute, value)
33
+ if value.present?
34
+ unless value.gsub(" ",'') == value
35
+ object.errors[attribute] << (options[:message] || "cannot contain any whitespace")
36
+ end
37
+ if [value[0], value[-1]].any?{ |x| x == "." }
38
+ object.errors[attribute] << (options[:message] || "cannot contain a period at the start or end")
39
+ end
40
+ unless value =~ /[a-zA-Z]/
41
+ object.errors[attribute] << (options[:message] || "must contain at least one letter")
42
+ end
43
+ unless value =~ /[0-9]/
44
+ object.errors[attribute] << (options[:message] || "must contain at least one number")
45
+ end
46
+ unless value =~ /[_\-.]/
47
+ object.errors[attribute] << (options[:message] || "must contain at least one these special characters \"-_.\"")
48
+ end
49
+ unless value =~ /^[a-zA-Z0-9_\-.]+$/
50
+ object.errors[attribute] << (options[:message] || "can only contain letters number and special characters \"-_.\"")
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ class UnchangeableValidator < ActiveModel::EachValidator
57
+ def validate_each(object, attribute, value)
58
+ if !object.new_record? && value.present?
59
+ original = object.class.send(:where, "id = #{object.id}").select("id, #{attribute.to_s}").first
60
+ if original.send(attribute) != value
61
+ object.errors[attribute] << (options[:message] || "cannot be changed once assigned")
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ module Protected
68
+ require File.dirname(__FILE__) + '/../../../lib/protected/devise_recoverable_extensions'
69
+ require File.dirname(__FILE__) + '/../../../lib/protected/password_utils'
70
+
71
+ class User < ActiveRecord::Base
72
+ # devise :database_authenticatable, :recoverable, :trackable, :validatable, :lockable, :timeoutable, :password_archivable,
73
+ # :maximum_attempts => 4, :unlock_strategy => :none
74
+ devise :database_authenticatable, :recoverable, :trackable, :validatable, :lockable, :timeoutable,
75
+ :maximum_attempts => 4, :unlock_strategy => :none
76
+
77
+ has_many :old_passwords, :as => :password_archivable, :dependent => :destroy
78
+
79
+ validates :first_name, :presence => true, :format => { :with => /^[a-zA-Z0-9_\s\-.]+$/ }
80
+ validates :last_name, :presence => true, :format => { :with => /^[a-zA-Z0-9_\s\-.]+$/ }, :uniqueness => { :scope => :first_name, :message => "has alredy been taken by another user with the same first name" }
81
+ validate :validate_password_strength, :if => lambda{new_record? || (not password.blank?)}
82
+ validates :login, :unchangeable => true, :presence => true, :login_format => true, :uniqueness => { :case_sensitive => false }, :length => { :maximum => 64, :minimum => 6 }
83
+ validates :email, :unchangeable => true
84
+
85
+ # Setup accessible (or protected) attributes for your model
86
+ attr_accessible :login, :email, :password, :password_confirmation, :remember_me, :first_name, :last_name, :company, :locked_at, :failed_attempts, :first_login, :current_password
87
+ attr_accessor :current_password
88
+
89
+ after_save :send_mail_if_needed
90
+
91
+ # TODO: Extract this code into security pack extension
92
+ def archive_password
93
+ if(self.encrypted_password_changed?)
94
+ self.old_passwords.create! :encrypted_password => self.encrypted_password, :password_salt => self.password_salt, :password_archivable_id => self.id, :password_archivable_type => self.class.to_s
95
+ end
96
+ end
97
+
98
+ def update_on_first_login!(modified_user)
99
+ if password_used?(modified_user[:password])
100
+ raise PasswordAlreadyUsedException
101
+ else
102
+ update_attributes!(
103
+ :password => modified_user[:password],
104
+ :password_confirmation => modified_user[:password_confirmation],
105
+ :first_login => false)
106
+ archive_password
107
+ end
108
+ end
109
+
110
+ def name
111
+ [first_name, last_name].join(' ')
112
+ end
113
+
114
+ def password_used?(password)
115
+ self.password = password
116
+ return password_archive_included?
117
+ end
118
+
119
+ def reset_password!(pass,conf)
120
+ if reset_password_token.present? and
121
+ pass == conf and
122
+ encrypted_password_changed? and
123
+ password_archive_included?
124
+ errors.add(:base, I18n.t('errors.messages.taken_in_past'))
125
+ else
126
+ super
127
+ end
128
+ self
129
+ end
130
+
131
+ def random_password
132
+ self.password = self.password_confirmation = PasswordUtils.generate_random_password
133
+ end
134
+
135
+ def send_mail_if_needed
136
+ if !self.changes.empty? &&
137
+ self.changes[:id].present? &&
138
+ self.changes[:id][0].nil? &&
139
+ self.changes[:id][1].present?
140
+ if self.first_login?
141
+ self.archive_password
142
+ UserMailer.welcome_login_instructions(self).deliver
143
+ UserMailer.welcome_password_instructions(self).deliver
144
+ end
145
+ end
146
+ end
147
+
148
+ class << self
149
+ def reset_password_and_send_password_instructions(params)
150
+ record = find_by_email(params["email"])
151
+ record.random_password && record.save unless record.nil?
152
+ send_reset_password_instructions(params)
153
+ end
154
+ end
155
+
156
+ # NOTE: To audit the devise actions we need to ensure we include this after devise has been added. This is a bug in the gem
157
+ # and should be fixed.
158
+ audit :methods => [:destroy, :unlock_access!], :attributes => [:first_name,:last_name, :company, :is_admin]
159
+
160
+ private
161
+ def validate_password_strength
162
+ errors.add(:password, I18n.t('devise.passwords.password_strength')) if not PasswordUtils.strong?(password)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,38 @@
1
+ !!!
2
+ %html{:lang => "en"}
3
+ %head
4
+ %meta{:charset => "utf-8"}
5
+ %title
6
+ %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
7
+ %meta{:content => "", :name => "description"}
8
+ %meta{:content => "", :name => "author"}
9
+ = stylesheet_link_tag "protected/application"
10
+
11
+ %body{ :class => "#{Rails.env} #{params[:controller]} #{params[:action]}" }
12
+ .navbar.navbar-fixed-top
13
+ .navbar-inner
14
+ .container-fluid
15
+ %a.btn.btn-navbar{"data-target" => ".nav-collapse", "data-toggle" => "collapse"}
16
+ %span.icon-bar
17
+ %span.icon-bar
18
+ %span.icon-bar
19
+ %a.brand{:href => "#"} Project name
20
+ .nav-collapse
21
+ - if current_user
22
+ %ul.nav
23
+ %li= link_to "Admin", admin_url
24
+ %li= link_to "Users", ['admin','users']
25
+ %li= link_to "Live Site", root_url
26
+ %ul.nav.right
27
+ %li= link_to "Account Settings", ['edit','admin',current_user]
28
+ %li= link_to 'Sign Out', destroy_user_session_url
29
+
30
+ .container-fluid
31
+ .row-fluid
32
+ - flash.each do |name, msg|
33
+ %div{:class => "alert alert-#{name == :notice ? "success" : "error"}"}
34
+ %a.close{"data-dismiss" => "alert"} ×
35
+ = content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String)
36
+ = yield
37
+
38
+ = javascript_include_tag "protected/application"