authkit 0.0.1

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 (38) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/FEATURES.md +73 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +168 -0
  7. data/Rakefile +60 -0
  8. data/authkit.gemspec +27 -0
  9. data/config/database.yml.example +19 -0
  10. data/lib/authkit.rb +5 -0
  11. data/lib/authkit/engine.rb +7 -0
  12. data/lib/authkit/version.rb +3 -0
  13. data/lib/generators/authkit/USAGE +18 -0
  14. data/lib/generators/authkit/install_generator.rb +113 -0
  15. data/lib/generators/authkit/templates/app/controllers/application_controller.rb +94 -0
  16. data/lib/generators/authkit/templates/app/controllers/email_confirmation_controller.rb +25 -0
  17. data/lib/generators/authkit/templates/app/controllers/password_change_controller.rb +29 -0
  18. data/lib/generators/authkit/templates/app/controllers/password_reset_controller.rb +29 -0
  19. data/lib/generators/authkit/templates/app/controllers/sessions_controller.rb +35 -0
  20. data/lib/generators/authkit/templates/app/controllers/users_controller.rb +89 -0
  21. data/lib/generators/authkit/templates/app/models/user.rb +170 -0
  22. data/lib/generators/authkit/templates/app/views/password_change/show.html.erb +16 -0
  23. data/lib/generators/authkit/templates/app/views/password_reset/show.html.erb +12 -0
  24. data/lib/generators/authkit/templates/app/views/sessions/new.html.erb +13 -0
  25. data/lib/generators/authkit/templates/app/views/users/edit.html.erb +58 -0
  26. data/lib/generators/authkit/templates/app/views/users/new.html.erb +58 -0
  27. data/lib/generators/authkit/templates/db/migrate/add_authkit_fields_to_users.rb +110 -0
  28. data/lib/generators/authkit/templates/db/migrate/create_users.rb +17 -0
  29. data/lib/generators/authkit/templates/lib/email_format_validator.rb +11 -0
  30. data/lib/generators/authkit/templates/spec/controllers/application_controller_spec.rb +188 -0
  31. data/lib/generators/authkit/templates/spec/controllers/email_confirmation_controller_spec.rb +80 -0
  32. data/lib/generators/authkit/templates/spec/controllers/password_change_controller_spec.rb +98 -0
  33. data/lib/generators/authkit/templates/spec/controllers/password_reset_controller_spec.rb +87 -0
  34. data/lib/generators/authkit/templates/spec/controllers/sessions_controller_spec.rb +111 -0
  35. data/lib/generators/authkit/templates/spec/controllers/users_controller_spec.rb +195 -0
  36. data/lib/generators/authkit/templates/spec/models/user_spec.rb +268 -0
  37. data/spec/spec_helper.rb +16 -0
  38. metadata +165 -0
@@ -0,0 +1,113 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+
4
+ module Authkit
5
+ class InstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ desc "An auth system for your Rails app"
9
+
10
+ def self.source_root
11
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
12
+ end
13
+
14
+ def generate_authkit
15
+ generate_migration("create_users")
16
+ generate_migration("add_authkit_fields_to_users")
17
+
18
+ # Ensure the destination structure
19
+ empty_directory "app"
20
+ empty_directory "app/models"
21
+ empty_directory "app/controllers"
22
+ empty_directory "app/views"
23
+ empty_directory "app/views/users"
24
+ empty_directory "app/views/sessions"
25
+ empty_directory "app/views/password_reset"
26
+ empty_directory "app/views/password_change"
27
+ empty_directory "spec"
28
+ empty_directory "spec/models"
29
+ empty_directory "spec/controllers"
30
+ empty_directory "lib"
31
+
32
+ # Fill out some templates (for now, this is just straight copy)
33
+ template "app/models/user.rb", "app/models/user.rb"
34
+ template "app/controllers/users_controller.rb", "app/controllers/users_controller.rb"
35
+ template "app/controllers/sessions_controller.rb", "app/controllers/sessions_controller.rb"
36
+ template "app/controllers/password_reset_controller.rb", "app/controllers/password_reset_controller.rb"
37
+ template "app/controllers/password_change_controller.rb", "app/controllers/password_change_controller.rb"
38
+ template "app/controllers/email_confirmation_controller.rb", "app/controllers/email_confirmation_controller.rb"
39
+
40
+ template "spec/models/user_spec.rb", "spec/models/user_spec.rb"
41
+ template "spec/controllers/application_controller_spec.rb", "spec/controllers/application_controller_spec.rb"
42
+ template "spec/controllers/users_controller_spec.rb", "spec/controllers/users_controller_spec.rb"
43
+ template "spec/controllers/sessions_controller_spec.rb", "spec/controllers/sessions_controller_spec.rb"
44
+ template "spec/controllers/password_reset_controller_spec.rb", "spec/controllers/password_reset_controller_spec.rb"
45
+ template "spec/controllers/password_change_controller_spec.rb", "spec/controllers/password_change_controller_spec.rb"
46
+ template "spec/controllers/email_confirmation_controller_spec.rb", "spec/controllers/email_confirmation_controller_spec.rb"
47
+
48
+ template "lib/email_format_validator.rb", "lib/email_format_validator.rb"
49
+
50
+ # Don't treat these like templates
51
+ copy_file "app/views/users/new.html.erb", "app/views/users/new.html.erb"
52
+ copy_file "app/views/users/edit.html.erb", "app/views/users/edit.html.erb"
53
+ copy_file "app/views/sessions/new.html.erb", "app/views/sessions/new.html.erb"
54
+ copy_file "app/views/password_reset/show.html.erb", "app/views/password_reset/show.html.erb"
55
+ copy_file "app/views/password_change/show.html.erb", "app/views/password_change/show.html.erb"
56
+
57
+ # We don't want to override this file and may have a protected section
58
+ insert_at_end_of_class "app/controllers/application_controller.rb", "app/controllers/application_controller.rb"
59
+
60
+ # Need a temp root
61
+ route "root 'welcome#index'"
62
+
63
+ # Setup the routes
64
+ route "get '/email/confirm/:token', to: 'email_confirmation#show', as: :confirm"
65
+
66
+ route "post '/password/reset', to: 'password_reset#create'"
67
+ route "get '/password/reset', to: 'password_reset#show', as: :password_reset"
68
+ route "post '/password/change/:token', to: 'password_change#create'"
69
+ route "get '/password/change/:token', to: 'password_change#show', as: :password_change"
70
+
71
+ route "get '/signup', to: 'users#new', as: :signup"
72
+ route "get '/logout', to: 'sessions#destroy', as: :logout"
73
+ route "get '/login', to: 'sessions#new', as: :login"
74
+
75
+ route "put '/account', to: 'users#update'"
76
+ route "get '/account', to: 'users#edit', as: :user"
77
+
78
+ route "resources :sessions, only: [:new, :create, :destroy]"
79
+ route "resources :users, only: [:new, :create]"
80
+
81
+ # Support for has_secure_password and has_one_time_password
82
+ gem "active_model_otp"
83
+ gem "bcrypt-ruby", '~> 3.0.0'
84
+
85
+ # RSpec needs to be in the development group to be used in generators
86
+ gem_group :test, :development do
87
+ gem "rspec-rails"
88
+ gem "shoulda-matchers"
89
+ end
90
+ end
91
+
92
+ def self.next_migration_number(dirname)
93
+ ActiveRecord::Generators::Base.next_migration_number(dirname)
94
+ end
95
+
96
+ protected
97
+
98
+ def insert_at_end_of_class(filename, source)
99
+ source = File.expand_path(find_in_source_paths(source.to_s))
100
+ context = instance_eval('binding')
101
+ content = ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
102
+ insert_into_file "app/controllers/application_controller.rb", "#{content}\n", before: /end\n*\z/
103
+ end
104
+
105
+ def generate_migration(filename)
106
+ if self.class.migration_exists?("db/migrate", "#{filename}")
107
+ say_status "skipped", "Migration #{filename}.rb already exists"
108
+ else
109
+ migration_template "db/migrate/#{filename}.rb", "db/migrate/#{filename}.rb"
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,94 @@
1
+
2
+ before_filter :set_time_zone
3
+
4
+ helper_method :logged_in?, :current_user
5
+
6
+ # It is very unlikely that this exception will be created under normal
7
+ # circumstances. Unique validations are handled in Rails, but they are
8
+ # also enforced at the database level to guarantee data integrity. In
9
+ # certain cases (double-clicking a save link, multiple distributed servers)
10
+ # it is possible to get past the Rails validation in which case the
11
+ # database throws an exception.
12
+ rescue_from ActiveRecord::RecordNotUnique, with: :record_not_unique
13
+
14
+ protected
15
+
16
+ def current_user
17
+ return @current_user if defined?(@current_user)
18
+ @current_user ||= User.find_by(session[:user_id]) if session[:user_id]
19
+ @current_user ||= User.user_from_remember_token(cookies.signed[:remember]) unless cookies.signed[:remember].blank?
20
+ session[:user_id] = @current_user.id if @current_user
21
+ session[:time_zone] = @current_user.time_zone if @current_user
22
+ set_time_zone
23
+
24
+ @current_user
25
+ end
26
+
27
+ def allow_tracking?
28
+ "#{request.headers['X-Do-Not-Track']}" != '1' && "#{request.headers['DNT']}" != '1'
29
+ end
30
+
31
+ def logged_in?
32
+ !!current_user
33
+ end
34
+
35
+ def require_login
36
+ deny_user(nil, login_path) unless logged_in?
37
+ end
38
+
39
+ def require_token
40
+ deny_user("Invalid token", root_path) unless @user = User.user_from_token(params[:token])
41
+ end
42
+
43
+ def login(user)
44
+ @current_user = user
45
+ current_user.track_sign_in(request.remote_ip) if allow_tracking?
46
+ current_user.set_token(:remember_token)
47
+ set_remember_cookie
48
+ reset_session
49
+ session[:user_id] = current_user.id
50
+ session[:time_zone] = current_user.time_zone
51
+ set_time_zone
52
+ current_user
53
+ end
54
+
55
+ def logout
56
+ current_user.clear_remember_token if current_user
57
+ cookies.delete(:remember)
58
+ reset_session
59
+ @current_user = nil
60
+ end
61
+
62
+ def set_time_zone
63
+ Time.zone = session[:time_zone] if session[:time_zone].present?
64
+ end
65
+
66
+ def set_remember_cookie
67
+ cookies.permanent.signed[:remember] = {
68
+ value: current_user.remember_token,
69
+ secure: Rails.env.production?
70
+ }
71
+ end
72
+
73
+ def redirect_back_or_default
74
+ redirect_to(session.delete(:return_url) || root_path)
75
+ end
76
+
77
+ def deny_user(message=nil, location=nil)
78
+ location ||= (logged_in? ? root_path : login_path)
79
+
80
+ session[:return_url] = request.fullpath
81
+ respond_to do |format|
82
+ format.json { render(status: 403, nothing: true) }
83
+ format.html do
84
+ flash[:error] = message || "Sorry, you must be logged in to do that"
85
+ redirect_to(location)
86
+ end
87
+ end
88
+
89
+ false
90
+ end
91
+
92
+ def record_not_unique
93
+ respond_with(nil, location: root_path, status: 422)
94
+ end
@@ -0,0 +1,25 @@
1
+ class EmailConfirmationController < ApplicationController
2
+ before_filter :require_token
3
+
4
+ respond_to :html
5
+
6
+ def show
7
+ if @user.email_confirmed
8
+ login(@user)
9
+ flash[:notice] = "Thanks for confirming your email address"
10
+ respond_to do |format|
11
+ format.json { head :no_content }
12
+ format.html { redirect_to root_path }
13
+ end
14
+ else
15
+ respond_to do |format|
16
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
17
+ format.html {
18
+ flash[:error] = "Could not confirm email address because it is already in use"
19
+ redirect_to root_path
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+
@@ -0,0 +1,29 @@
1
+ class PasswordChangeController < ApplicationController
2
+ before_filter :require_token
3
+
4
+ def show
5
+ respond_to do |format|
6
+ format.json { head :no_content }
7
+ format.html
8
+ end
9
+ end
10
+
11
+ def create
12
+ if @user.change_password(params[:password], params[:password_confirmation])
13
+ login(@user)
14
+
15
+ respond_to do |format|
16
+ format.json { head :no_content }
17
+ format.html {
18
+ flash.now[:notice] = "Password updated successfully"
19
+ redirect_to(root_path)
20
+ }
21
+ end
22
+ else
23
+ respond_to do |format|
24
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
25
+ format.html { render :show }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ class PasswordResetController < ApplicationController
2
+ def show
3
+ end
4
+
5
+ def create
6
+ username_or_email = "#{params[:email]}".downcase
7
+ user = User.find_by_username_or_email(username_or_email) if username_or_email.present?
8
+
9
+ if user && user.send_reset_password
10
+ logout
11
+
12
+ respond_to do |format|
13
+ format.json { head :no_content }
14
+ format.html {
15
+ flash[:notice] = "We've sent an email which can be used to change your password"
16
+ redirect_to login_path
17
+ }
18
+ end
19
+ else
20
+ respond_to do |format|
21
+ format.json { render json: { errors: ["Invalid user name or email"], status: "error" }, status: 422 }
22
+ format.html {
23
+ flash.now[:error] = "Invalid user name or email"
24
+ render :show
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ class SessionsController < ApplicationController
2
+ # Login
3
+ def new
4
+ end
5
+
6
+ def create
7
+ username_or_email = "#{params[:email]}".downcase
8
+ user = User.find_by_username_or_email(username_or_email) if username_or_email.present?
9
+
10
+ if user && user.authenticate(params[:password])
11
+ login(user)
12
+ respond_to do |format|
13
+ format.json { head :no_content }
14
+ format.html { redirect_back_or_default }
15
+ end
16
+ else
17
+ respond_to do |format|
18
+ format.json { render json: { errors: ["Invalid user name or password"], status: "error" }, status: 422 }
19
+ format.html {
20
+ flash.now[:error] = "Invalid user name or password"
21
+ render :new
22
+ }
23
+ end
24
+ end
25
+ end
26
+
27
+ # Logout
28
+ def destroy
29
+ logout
30
+ respond_to do |format|
31
+ format.json { head :no_content }
32
+ format.html { redirect_to root_path }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,89 @@
1
+ class UsersController < ApplicationController
2
+ before_filter :require_login, only: [:edit, :update]
3
+
4
+ respond_to :html, :json
5
+
6
+ # Signup
7
+ def new
8
+ @user = User.new
9
+ end
10
+
11
+ def create
12
+ @user = User.new(user_create_params)
13
+ if @user.save
14
+ @user.send_confirmation
15
+ login(@user)
16
+ respond_to do |format|
17
+ format.json { head :no_content }
18
+ format.html { redirect_to root_path }
19
+ end
20
+ else
21
+ respond_to do |format|
22
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
23
+ format.html { render :new }
24
+ end
25
+ end
26
+ end
27
+
28
+ def edit
29
+ @user = current_user
30
+ end
31
+
32
+ def update
33
+ @user = current_user
34
+
35
+ orig_confirmation_email = @user.confirmation_email
36
+
37
+ if @user.update_attributes(user_update_params)
38
+ # Send a new email confirmation if the user updated their email address
39
+ if @user.confirmation_email.present? &&
40
+ @user.confirmation_email != @user.email &&
41
+ @user.confirmation_email != orig_confirmation_email
42
+ @user.send_confirmation
43
+ end
44
+ respond_to do |format|
45
+ format.json { head :no_content }
46
+ format.html { redirect_to @user }
47
+ end
48
+ else
49
+ respond_to do |format|
50
+ format.json { render json: { status: 'error', errors: @user.errors }.to_json, status: 422 }
51
+ format.html { render :edit }
52
+ end
53
+ end
54
+ end
55
+
56
+ protected
57
+
58
+ # It would be nice to find a strategy to merge these. The only difference is that
59
+ # when signing up you are setting the email, and when changing your settings you
60
+ # are setting the confirmation email.
61
+
62
+ def user_create_params
63
+ params.require(:user).permit(
64
+ :email,
65
+ :username,
66
+ :password,
67
+ :password_confirmation,
68
+ :first_name,
69
+ :last_name,
70
+ :bio,
71
+ :website,
72
+ :phone_number,
73
+ :time_zone)
74
+ end
75
+
76
+ def user_update_params
77
+ params.require(:user).permit(
78
+ :confirmation_email,
79
+ :username,
80
+ :password,
81
+ :password_confirmation,
82
+ :first_name,
83
+ :last_name,
84
+ :bio,
85
+ :website,
86
+ :phone_number,
87
+ :time_zone)
88
+ end
89
+ end
@@ -0,0 +1,170 @@
1
+ require 'email_format_validator'
2
+
3
+ class User < ActiveRecord::Base
4
+ has_secure_password
5
+ has_one_time_password
6
+
7
+ # Uncomment if you are not using strong params (note, that email is only permitted on
8
+ # signup and confirmation_email is only permitted on update):
9
+ #
10
+ # attr_accessible :username,
11
+ # :email,
12
+ # :confirmation_email,
13
+ # :password,
14
+ # :password_confirmation,
15
+ # :time_zone,
16
+ # :first_name,
17
+ # :last_name,
18
+ # :bio,
19
+ # :website,
20
+ # :phone_number
21
+
22
+ before_validation :downcase_email
23
+ before_validation :set_confirmation_email
24
+
25
+ # Whenever the password is set, validate (not only on create)
26
+ validates :password, presence: true, confirmation: true, length: {minimum: 6}, if: :password_set?
27
+ validates :username, presence: true, uniqueness: {case_sensitive: false}
28
+ validates :email, email_format: true, presence: true, uniqueness: true
29
+ validates :confirmation_email, email_format: true, presence: true
30
+
31
+ # Confirm emails check for existing emails for uniqueness as a convenience
32
+ validate :confirmation_email_uniqueness, if: :confirmation_email_set?
33
+
34
+ def self.user_from_token(token)
35
+ verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
36
+ id = verifier.verify(token)
37
+ User.find_by_id(id)
38
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
39
+ nil
40
+ end
41
+
42
+ # The tokens created by this method have unique indexes but they are digests of the
43
+ # id which is unique. Because of this we shouldn't see a conflict. If we do, however
44
+ # we want the ActiveRecord::StatementInvalid or ActiveRecord::RecordNotUnique exeception
45
+ # to bubble up.
46
+ def set_token(field)
47
+ return unless self.persisted?
48
+ verifier = ActiveSupport::MessageVerifier.new(Rails.application.config.secret_token)
49
+ self.send("#{field}_created_at=", Time.now)
50
+ self.send("#{field}=", verifier.generate(self.id))
51
+ self.save
52
+ end
53
+
54
+ # These methods are a little redundant, but give you the opportunity to
55
+ # insert expiry for any of these token based authentication strategies.
56
+ # For example:
57
+ #
58
+ # def self.user_from_remember_token(token)
59
+ # user = user_from_token(token)
60
+ # user = nil if user && user.remember_token_created_at < 30.days.ago
61
+ # user
62
+ # end
63
+ #
64
+ def self.user_from_remember_token(token)
65
+ user_from_token(token)
66
+ end
67
+
68
+ def self.user_from_reset_password_token(token)
69
+ user_from_token(token)
70
+ end
71
+
72
+ def self.user_from_confirmation_token(token)
73
+ user_from_token(token)
74
+ end
75
+
76
+ def self.user_from_unlock_token(token)
77
+ user_from_token(token)
78
+ end
79
+
80
+ def display_name
81
+ [first_name, last_name].compact.join(" ")
82
+ end
83
+
84
+ def track_sign_in(ip)
85
+ self.sign_in_count += 1
86
+ self.last_sign_in_at = self.current_sign_in_at
87
+ self.last_sign_in_ip = self.current_sign_in_ip
88
+ self.current_sign_in_at = Time.now
89
+ self.current_sign_in_ip = ip
90
+ self.save
91
+ end
92
+
93
+ def clear_remember_token
94
+ self.remember_token = nil
95
+ self.remember_token_created_at = nil
96
+ self.save
97
+ end
98
+
99
+ def send_reset_password
100
+ return false unless set_token(:reset_password_token)
101
+
102
+ # TODO: insert your mailer logic here
103
+ true
104
+ end
105
+
106
+ def send_confirmation
107
+ return false unless set_token(:confirmation_token)
108
+
109
+ # TODO: insert your mailer logic here
110
+ true
111
+ end
112
+
113
+ def email_confirmed
114
+ return false if self.confirmation_token.blank? || self.confirmation_email.blank?
115
+
116
+ self.email = self.confirmation_email
117
+
118
+ # Don't nil out the token unless the changes are valid as it may be
119
+ # needed again (when re-rendering the form, for instance)
120
+ if valid?
121
+ self.confirmation_token = nil
122
+ self.confirmation_token_created_at = nil
123
+ end
124
+
125
+ self.save
126
+ end
127
+
128
+ def change_password(password, password_confirmation)
129
+ self.password = password
130
+ self.password_confirmation = password_confirmation
131
+
132
+ # Don't nil out the token unless the changes are valid as it may be
133
+ # needed again (when re-rendering the form, for instance)
134
+ if valid?
135
+ self.reset_password_token = nil
136
+ self.reset_password_token_created_at = nil
137
+ end
138
+
139
+ self.save
140
+ end
141
+
142
+ protected
143
+
144
+ def password_set?
145
+ self.password.present?
146
+ end
147
+
148
+ def downcase_email
149
+ self.email = self.email.downcase if self.email
150
+ end
151
+
152
+ def set_confirmation_email
153
+ self.confirmation_email = self.email if self.confirmation_email.blank?
154
+ end
155
+
156
+ def confirmation_email_set?
157
+ confirmation_email.present? && confirmation_email_changed? && confirmation_email != email
158
+ end
159
+
160
+ # It is possible that a user will change their email, not confirm, and then
161
+ # sign up for the service again using the same email. If they later go to confirm
162
+ # the email change on the first account it will fail because the email will be
163
+ # used by the new signup. Though this is problematic it avoids the larger problem of
164
+ # users blocking new user signups by changing their email address to something they
165
+ # don't control. This check is just for convenience and does not need to
166
+ # guarantee uniqueness.
167
+ def confirmation_email_uniqueness
168
+ errors.add(:confirmation_email, :taken, value: email) if User.where('email = ?', confirmation_email).count > 0
169
+ end
170
+ end