login_sugar_generator 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
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