login_sugar_generator 0.9.4

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.
data/USAGE ADDED
@@ -0,0 +1,32 @@
1
+ NAME
2
+ login_sugar - creates a functional login system with email validation and salted hash passwords
3
+
4
+ SYNOPSIS
5
+ login_sugar [Controller name]
6
+
7
+ Good names are User, Account, Person, etc
8
+
9
+ See README_LOGIN_SUGAR for configuration instructions.
10
+
11
+ DESCRIPTION
12
+ This generator creates a general purpose login system.
13
+
14
+ Included:
15
+ - a model which uses SHA1 encryption and salted hashes for passwords
16
+ - a controller with signup, login, welcome and logoff actions
17
+ - a mailer that integrates with the controller to prevent script based
18
+ account creation (i.e., requires account verification from the
19
+ registered email address) and supports forgotten and changed passwords
20
+ - a mixin which lets you easily add advanced authentication
21
+ features to your abstract base controller
22
+ - an example database migration script with the minimal sql required to
23
+ get the model to work.
24
+ - extensive unit and functional test cases to make sure nothing breaks.
25
+ - token based authentication
26
+
27
+ EXAMPLE
28
+ ./script/generate login_sugar User
29
+
30
+ This will generate a User controller with login and logout methods.
31
+ The class names are UserController, User (model), and UserNotifier
32
+ (mailer). It will also generate a module named UserLoginSystem.
@@ -0,0 +1,79 @@
1
+ class LoginSugarGenerator < Rails::Generator::NamedBase
2
+
3
+ attr_accessor :controller_class_name
4
+ #
5
+ #TODO
6
+ # - rakify try_it.sh
7
+ # - store user.id in session
8
+
9
+ def manifest
10
+ record do |m|
11
+ # Check for class naming collisions.
12
+ #m.class_collisions class_path, "#{class_name}Controller", "#{class_name}ControllerTest", "#{class_name}Helper", "#{class_name}LoginSystem"
13
+
14
+ # Login module, controller class, functional test, and helper.
15
+ m.template "login_system.rb", "lib/#{file_name}_system.rb"
16
+ m.template "controller.rb", File.join("app/controllers", class_path, "#{file_name}_controller.rb")
17
+ m.template "controller_test.rb", "test/functional/#{file_name}_controller_test.rb"
18
+ m.template "integration_test.rb", "test/integration/#{file_name}_system_test.rb"
19
+ m.template "helper.rb", File.join("app/helpers", class_path, "#{file_name}_helper.rb")
20
+
21
+ # Model class, unit test, fixtures, and example schema.
22
+ m.template "user.rb", File.join("app/models", class_path, "#{file_name}.rb")
23
+ m.template "notify.rb", File.join("app/models", class_path, "#{file_name}_notify.rb")
24
+ m.template "mock_notify.rb", "test/mocks/test/#{file_name}_notify.rb"
25
+ m.file "mock_clock.rb", "test/mocks/test/clock.rb"
26
+ m.template "clock.rb", "lib/clock.rb"
27
+
28
+ m.template "user_test.rb", "test/unit/#{file_name}_test.rb"
29
+ m.template "users.yml", "test/fixtures/#{plural_name}.yml"
30
+ m.directory "db/migrate"
31
+ m.template "migration_login_sugar.rb", "db/migrate/migration_login_sugar__rename_this_to_fit_your_project.rb"
32
+
33
+ # Configuration and miscellaneous
34
+ m.template "login_environment.rb", "config/environments/#{file_name}_environment.rb"
35
+ #m.file "create_db", "script/create_db"
36
+ #m.template "en.yaml", "lang/en.yaml"
37
+ m.file "default_setup.zip", "default_login_sugar_setup.zip"
38
+
39
+ # Layout and stylesheet.
40
+ m.template "layout.rhtml", "app/views/layouts/#{file_name}.rhtml"
41
+ m.directory "public/stylesheets"
42
+ m.template "stylesheet.css", "public/stylesheets/#{file_name}.css"
43
+
44
+ # Views.
45
+ m.directory File.join("app/views", class_path, file_name)
46
+ login_views.each do |action|
47
+ m.template "view_#{action}.rhtml",
48
+ File.join("app/views", class_path, file_name, "#{action}.rhtml")
49
+ end
50
+
51
+ # Partials
52
+ m.directory File.join("app/views", class_path, file_name)
53
+ partial_views.each do |action|
54
+ m.template "_view_#{action}.rhtml",
55
+ File.join("app/views", class_path, file_name, "_#{action}.rhtml")
56
+ end
57
+
58
+ m.directory File.join("app/views", "#{singular_name}_notify")
59
+ notify_views.each do |action|
60
+ m.template "notify_#{action}.rhtml",
61
+ File.join("app/views", "#{singular_name}_notify", "#{action}.rhtml")
62
+ end
63
+
64
+ m.template "README", "README_LOGIN_SUGAR"
65
+ end
66
+ end
67
+
68
+ def login_views
69
+ %w(welcome login logout edit signup forgot_password change_password)
70
+ end
71
+
72
+ def partial_views
73
+ %w(edit password)
74
+ end
75
+
76
+ def notify_views
77
+ %w(signup forgot_password change_password)
78
+ end
79
+ end
data/templates/README ADDED
@@ -0,0 +1,194 @@
1
+ == About
2
+
3
+ login_sugar is a modification of salted_login generator 1.1.1 that works
4
+ out of the box on Rails 1.1.6
5
+
6
+ Changes:
7
+ - tests all pass out of the box on Rails >= 1.1.4
8
+ - using ActiveRecord::Migrations for db setup
9
+ - put underscores in first_name and last_name user attributes
10
+ - replaced Mock Time extension with a Mock Clock.
11
+ - README_USER_LOGIN is a one stop readme
12
+ - contains a default configuration zip
13
+ - session references se symbols
14
+ - localization removed
15
+
16
+ More about salted_login_generator at http://rubyforge.org/projects/salted-login
17
+
18
+ == Prerequisite
19
+
20
+ If you are on Windoze, see http://wiki.rubyonrails.com/rails/pages/iconv/
21
+
22
+ == Installation
23
+
24
+ If you are working with a fresh rails 1.1.6 project, you can unzip the included
25
+ default_login_sugar_setup.zip file in your RAILS_ROOT directory. It contains
26
+ preconfigured application controller, environment.rb, application_helper.rb and
27
+ test_helper.rb that should work if you used User as your controller names when
28
+ running the generator. If you do this, you can skip down to the PHASE II
29
+ section below. BUT DON'T DO THIS IF YOU HAVE ALREADY MODIFIED YOUR RAILS APP
30
+ as it will overwrite your existing files. Of course, you should have those
31
+ under source control anyhow...
32
+
33
+ - PHASE I -
34
+
35
+ After generating the login system, edit your app/controllers/application.rb
36
+ file. The beginning of your ApplicationController should look something like
37
+ this:
38
+
39
+ require '<%= file_name %>_system'
40
+
41
+ class ApplicationController < ActionController::Base
42
+ include <%= class_name %>System
43
+ helper :<%= singular_name %>
44
+ model :<%= singular_name %>
45
+ before_filter :authenticate_user
46
+
47
+ Add the following at the end of your config/environment.rb file:
48
+
49
+ require 'environments/<%= singular_name %>_environment'
50
+
51
+ Add the following line to the top of your test/test_helper.rb:
52
+
53
+ require 'user_notify'
54
+
55
+ - PHASE II -
56
+
57
+ Under the 'environments' subdirectory, you'll find <%= singular_name %>_environment.rb.
58
+ Edit this file as necessary.
59
+
60
+ Import the <%= singular_name %> model into the database.
61
+
62
+ You'll have to create your development and test databases first and configure your
63
+ config/database.yml appropriately. Note, if you are using mysql you will need to edit
64
+ test/fixtures/users.yml and make the indicated change on the last fixture.
65
+
66
+ You can use the provided migration script in
67
+ db/migrate/migration_login_sugar__rename_this_to_fit_your_project.rb.
68
+
69
+ This model is meant as an example and you
70
+ can extend it, however I suggest first completing the stock installation and running
71
+ the tests to confirm installation, then create a new migration to add your new columns.
72
+
73
+ Rename db/migrate/migration_login_sugar__rename_this_to_fit_your_project.rb to
74
+ db/migrate/###_login_sugar.rb where ### is the proper new migration level. For example,
75
+ if this is your first migration for this rails project, use 001. If you name it something
76
+ other than ###_login_sugar.rb then you'll need to edit the file and change the class name
77
+ of the migration in the class definition as well.
78
+
79
+ Then run:
80
+
81
+ rake migrate && rake db:test:clone
82
+
83
+ Go ahead and run the unit and functional tests now:
84
+
85
+ rake
86
+
87
+ These should all pass.
88
+
89
+ Finally, you must properly configure ActionMailer for your mail settings. For
90
+ example, I have the following in config/environments/development.rb (for a
91
+ .Mac account, and without my username and password, obviously):
92
+
93
+ ActionMailer::Base.server_settings = {
94
+ :address => "smtp.mac.com",
95
+ :port => 25,
96
+ :domain => "smtp.mac.com",
97
+ :user_name => "<your user name here>",
98
+ :password => "<your password here>",
99
+ :authentication => :login
100
+ }
101
+
102
+ You'll need to configure it properly so that email can be sent. One of the
103
+ easiest ways to test your configuration is to temporarily reraise exceptions
104
+ from the signup method (so that you get the actual mailer exception string).
105
+ In the rescue statement, put a single "raise" statement in. Once you've
106
+ debugged any setting problems, remove that statement to get the proper flash
107
+ error handling back.
108
+
109
+ == How to use it
110
+
111
+ Now you can go around and happily add "before_filter :authenticate_user" to the
112
+ controllers which you would like to protect.
113
+
114
+ After integrating the login system with your rails application navigate to your
115
+ new controller's signup method. There you can create a new account. After you
116
+ are done you should have a look at your DB. Your freshly created <%= singular_name %>
117
+ will be there but the password will be a sha1 hashed 40 digit mess. I find
118
+ this should be the minimum of security which every page offering login &
119
+ password should give its customers. Now you can move to one of those
120
+ controllers which you protected with the before_filter :authenticate_user snippet.
121
+ You will automatically be re-directed to your freshly created login controller
122
+ and you are asked for a password. After entering valid account data you will be
123
+ taken back to the controller which you requested earlier. Simple huh?
124
+
125
+ == Tips & Tricks
126
+
127
+ How do I...
128
+
129
+ ... access the user who is currently logged in
130
+
131
+ A: You can get the <%= singular_name %> object from the session using @session[:<%= singular_name %>]
132
+ Example:
133
+ Welcome <%%= @session[:<%= singular_name %>].name %>
134
+
135
+ ... restrict access to only a few methods?
136
+
137
+ A: Use before_filters build in scoping.
138
+ Example:
139
+ before_filter :authenticate_user, :only => [:myaccount, :changepassword]
140
+ before_filter :authenticate_user, :except => [:index]
141
+
142
+ ... check if a user is logged-in in my views?
143
+
144
+ A: @session[:<%= singular_name %>] will tell you. Here is an example helper which you can use to make this more pretty:
145
+ Example:
146
+ def <%= singular_name %>?
147
+ !@session[:<%= singular_name %>].nil?
148
+ end
149
+
150
+ ... return a user to the page they came from before logging in?
151
+ F
152
+ A: The user will be send back to the last url which called the method "store_location"
153
+ Example:
154
+ User was at /articles/show/1, wants to log in.
155
+ in articles_controller.rb, add store_location to the show function and
156
+ send the user to the login form.
157
+ After he logs in he will be send back to /articles/show/1
158
+
159
+ You can find more help at http://wiki.rubyonrails.com/rails/show/SaltedLoginGenerator
160
+
161
+ == Troubleshooting
162
+
163
+ One of the more common problems people have seen is that after verifying an
164
+ account by following the emailed URL, they are unable to login via the
165
+ normal login method since the verified field is not properly set in the
166
+ <%= singular_name %> model's row in the DB.
167
+
168
+ The most common cause of this problem is that the DB and session get out of
169
+ sync. In particular, it always happens for me after recreating the DB if I
170
+ have run the server previously. To fix the problem, remove the /tmp/ruby*
171
+ session files (from wherever they are for your installation) while the server
172
+ is stopped, and then restart. This usually is the cause of the problem.
173
+
174
+ A forthcoming release will probably fix this via a well placed reset_session
175
+ call (or requirement to add it after running the generator) so that it is done
176
+ automatically on startup.
177
+
178
+ == Changelog
179
+
180
+ login_sugar
181
+ 0.9.4 removed scaffold references, removed localization, added generator rake task
182
+ 0.9.3 fixed Clock.now, symbolized session references
183
+ 0.9.2 fixed localization reference for sign in form (thanks Nym)
184
+ 0.9.1 fixed double first_name replacing last_name (thanks BobF)
185
+ 0.9.0 first release, modified salted_login 1.1.1
186
+
187
+ salted_login
188
+
189
+ 1.0.9 Fixed hardcoded generator name (in controller test and schema) and README
190
+ 1.0.8 Generator/schema fixes and some README fixes/improvements
191
+ 1.0.7 Fixed bad bug with missing attr_accessor :new_password in user class
192
+ 1.0.6 Proper delete support and bug fixes
193
+ 1.0.5 Lots of fixes and changes (see rubyforge.org/salted-login)
194
+ 1.0.0 First gem release
@@ -0,0 +1,30 @@
1
+ <div class="<%= singular_name %>_edit">
2
+ <%%= hidden_field '<%= singular_name %>', 'form', :value => 'edit' %>
3
+ <table>
4
+ <tr class="two_columns">
5
+ <td class="prompt"><label>First Name:</label></td>
6
+ <td class="value"><%%= text_field '<%= singular_name %>', 'first_name' %></td>
7
+ </tr>
8
+ <tr class="two_columns">
9
+ <td class="prompt"><label>Last Name:</label></td>
10
+ <td class="value"><%%= text_field '<%= singular_name %>', 'last_name' %></td>
11
+ </tr>
12
+ <tr class="two_columns">
13
+ <td class="prompt"><label>Login:</label></td>
14
+ <td class="value">
15
+ <%%= @<%= singular_name %>.new_record? ? text_field( '<%= singular_name %>', 'login' ) : @<%= singular_name %>.login %>
16
+ </td>
17
+ </tr>
18
+ <tr class="two_columns">
19
+ <td class="prompt"><label>Email:</label></td>
20
+ <td class="value"><%%= text_field '<%= singular_name %>', 'email' %></td>
21
+ </tr>
22
+ <%% if submit %>
23
+ <tr>
24
+ <td>
25
+ <%%= submit_tag <%= singular_name %>.new_record? ? 'signup' : 'change_settings', :class => 'two_columns' %>
26
+ </td>
27
+ </tr>
28
+ <%% end %>
29
+ </table>
30
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="<%= singular_name %>_password">
2
+ <%%= hidden_field '<%= singular_name %>', 'form', :value => 'change_password' %>
3
+
4
+ <table>
5
+ <tr class="two_columns">
6
+ <td class="prompt"><label>Password:</label></td>
7
+ <td class="value"><%%= password_field '<%= singular_name %>', 'password', :size => 30 %></td>
8
+ </tr>
9
+ <tr class="two_columns">
10
+ <td class="prompt"><label>Password confirmation:</label></td>
11
+ <td class="value"><%%= password_field '<%= singular_name %>', 'password_confirmation', :size => 30 %></td>
12
+ </tr>
13
+ <%% if submit %>
14
+ <tr>
15
+ <td>
16
+ <%%= submit_tag 'change_password' %>
17
+ </td>
18
+ </tr>
19
+ <%% end %>
20
+ </table>
21
+ </div>
@@ -0,0 +1,14 @@
1
+ class Clock
2
+ def self.at( *params )
3
+ #TODO fix this
4
+ eval("Time.at #{params.join(',')}")
5
+ end
6
+
7
+ def self.now
8
+ Time.now
9
+ end
10
+
11
+ def self.time=
12
+ raise "Cannot set real Clock class"
13
+ end
14
+ end
@@ -0,0 +1,179 @@
1
+ class <%= class_name %>Controller < ApplicationController
2
+ layout '<%= singular_name %>'
3
+
4
+ skip_before_filter :authenticate_<%= singular_name %>, :only => [ :login, :signup, :forgot_password ]
5
+
6
+ def login
7
+ return if generate_blank_form
8
+ @<%= singular_name %> = <%= class_name %>.new(@params['<%= singular_name %>'])
9
+ <%= singular_name %> = <%= class_name %>.authenticate(@params['<%= singular_name %>']['login'], @params['<%= singular_name %>']['password'])
10
+ if <%= singular_name %>
11
+ @current_<%= singular_name %> = <%= singular_name %>
12
+ @session[:<%= singular_name %>_id] = <%= singular_name %>.id
13
+ flash['notice'] = 'Login succeeded'
14
+ redirect_back_or_default :action => 'welcome'
15
+ else
16
+ @login = @params['<%= singular_name %>']['login']
17
+ flash['message'] = 'Login failed'
18
+ end
19
+ end
20
+
21
+ def signup
22
+ return if generate_blank_form
23
+ @params['<%= singular_name %>'].delete('form')
24
+ @<%= singular_name %> = <%= class_name %>.new(@params['<%= singular_name %>'])
25
+ begin
26
+ <%= class_name %>.transaction(@<%= singular_name %>) do
27
+ @<%= singular_name %>.password_needs_confirmation = true
28
+ if @<%= singular_name %>.save
29
+ key = @<%= singular_name %>.generate_security_token
30
+ url = url_for(:action => 'welcome')
31
+ url += "?<%= singular_name %>[id]=#{@<%= singular_name %>.id}&key=#{key}"
32
+ <%= class_name %>Notify.deliver_signup(@<%= singular_name %>, @params['<%= singular_name %>']['password'], url)
33
+ flash['notice'] = 'Signup successful! Please check your registered email account to verify your account registration and continue with the login.'
34
+ redirect_to :action => 'login'
35
+ end
36
+ end
37
+ rescue Exception => ex
38
+ report_exception ex
39
+ flash['message'] = 'Error creating account: confirmation email not sent'
40
+ end
41
+ end
42
+
43
+ def logout
44
+ @session[:<%= singular_name %>_id] = nil
45
+ @current_<%= singular_name %> = nil
46
+ redirect_to :action => 'login'
47
+ end
48
+
49
+ def change_password
50
+ return if generate_filled_in
51
+ @params['<%= singular_name %>'].delete('form')
52
+ begin
53
+ @<%= singular_name %>.change_password(@params['<%= singular_name %>']['password'], @params['<%= singular_name %>']['password_confirmation'])
54
+ @<%= singular_name %>.save!
55
+ rescue Exception => ex
56
+ report_exception ex
57
+ flash.now['message'] = 'Your password could not be changed at this time. Please retry.'
58
+ render and return
59
+ end
60
+ begin
61
+ <%= class_name %>Notify.deliver_change_password(@<%= singular_name %>, @params['<%= singular_name %>']['password'])
62
+ rescue Exception => ex
63
+ report_exception ex
64
+ end
65
+
66
+ end
67
+
68
+ def forgot_password
69
+ if authenticated_<%= singular_name %>?
70
+ flash['message'] = 'You are currently logged in. You may change your password now.'
71
+ redirect_to :action => 'change_password'
72
+ return
73
+ end
74
+
75
+ return if generate_blank_form
76
+
77
+ if @params['<%= singular_name %>']['email'].empty?
78
+ flash.now['message'] = 'Please enter a valid email address.'
79
+ elsif (<%= singular_name %> = <%= class_name %>.find_by_email(@params['<%= singular_name %>']['email'])).nil?
80
+ flash.now['message'] = "We could not find a <%= singular_name %> with the email address #{CGI.escapeHTML(@params['<%= singular_name %>']['email'])}"
81
+ else
82
+ begin
83
+ <%= class_name %>.transaction(<%= singular_name %>) do
84
+ key = <%= singular_name %>.generate_security_token
85
+ url = url_for(:action => 'change_password')
86
+ url += "?<%= singular_name %>[id]=#{<%= singular_name %>.id}&key=#{key}"
87
+ <%= class_name %>Notify.deliver_forgot_password(<%= singular_name %>, url)
88
+ flash['notice'] = "Instructions on resetting your password have been emailed to #{CGI.escapeHTML(@params['<%= singular_name %>']['email'])}."
89
+ unless authenticated_<%= singular_name %>?
90
+ redirect_to :action => 'login'
91
+ return
92
+ end
93
+ redirect_back_or_default :action => 'welcome'
94
+ end
95
+ rescue Exception => ex
96
+ report_exception ex
97
+ flash.now['message'] = "Your password could not be emailed to #{CGI.escapeHTML(@params['<%= singular_name %>']['email'])}"
98
+ end
99
+ end
100
+ end
101
+
102
+ def edit
103
+ return if generate_filled_in
104
+ if @params['<%= singular_name %>']['form']
105
+ form = @params['<%= singular_name %>'].delete('form')
106
+ begin
107
+ case form
108
+ when "edit"
109
+ changeable_fields = ['first_name', 'last_name', 'email']
110
+ params = @params['<%= singular_name %>'].delete_if { |k,v| not changeable_fields.include?(k) }
111
+ @<%= singular_name %>.attributes = params
112
+ @<%= singular_name %>.save
113
+ flash.now['notice'] = "<%= class_name %> has been updated."
114
+ when "change_password"
115
+ change_password
116
+ when "delete"
117
+ delete
118
+ else
119
+ raise "unknown edit action"
120
+ end
121
+ rescue Exception => ex
122
+ logger.warn ex
123
+ logger.warn ex.backtrace
124
+ end
125
+ end
126
+ end
127
+
128
+ def delete
129
+ @<%= singular_name %> = @current_<%= singular_name %> || <%= class_name %>.find_by_id( @session[:<%= singular_name %>_id] )
130
+ begin
131
+ @<%= singular_name %>.update_attribute( :deleted, true )
132
+ logout
133
+ rescue Exception => ex
134
+ flash.now['message'] = "Error: #{@ex}."
135
+ redirect_back_or_default :action => 'welcome'
136
+ end
137
+ end
138
+
139
+ def welcome
140
+ end
141
+
142
+ protected
143
+
144
+ def protect?(action)
145
+ if ['login', 'signup', 'forgot_password'].include?(action)
146
+ return false
147
+ else
148
+ return true
149
+ end
150
+ end
151
+
152
+ # Generate a template <%= singular_name %> for certain actions on get
153
+ def generate_blank_form
154
+ case @request.method
155
+ when :get
156
+ @<%= singular_name %> = <%= class_name %>.new
157
+ render
158
+ return true
159
+ end
160
+ return false
161
+ end
162
+
163
+ # Generate a template <%= singular_name %> for certain actions on get
164
+ def generate_filled_in
165
+ @<%= singular_name %> = @current_<%= singular_name %> || <%= class_name %>.find_by_id( @session[:<%= singular_name %>_id] )
166
+ case @request.method
167
+ when :get
168
+ render
169
+ return true
170
+ end
171
+ return false
172
+ end
173
+
174
+ def report_exception( ex )
175
+ logger.warn ex
176
+ logger.warn ex.backtrace.join("\n")
177
+ end
178
+
179
+ end