merb-auth-slice-password-reset 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/.gitignore +15 -0
  2. data/LICENSE +20 -0
  3. data/README.textile +219 -0
  4. data/Rakefile +41 -0
  5. data/TODO +2 -0
  6. data/app/controllers/application.rb +5 -0
  7. data/app/controllers/passwords.rb +49 -0
  8. data/app/helpers/application_helper.rb +64 -0
  9. data/app/helpers/mailer_helper.rb +28 -0
  10. data/app/mailers/password_reset_mailer.rb +13 -0
  11. data/app/mailers/views/password_reset_mailer/password_reset.text.erb +3 -0
  12. data/app/views/layout/merb_auth_slice_password_reset.html.erb +16 -0
  13. data/app/views/passwords/forgot_password.html.erb +7 -0
  14. data/app/views/passwords/reset.html.erb +15 -0
  15. data/config/init.rb +80 -0
  16. data/lib/merb-auth-slice-password-reset.rb +82 -0
  17. data/lib/merb-auth-slice-password-reset/merbtasks.rb +112 -0
  18. data/lib/merb-auth-slice-password-reset/mixins/senile_user.rb +81 -0
  19. data/lib/merb-auth-slice-password-reset/mixins/senile_user/ar_senile_user.rb +19 -0
  20. data/lib/merb-auth-slice-password-reset/mixins/senile_user/dm_senile_user.rb +22 -0
  21. data/lib/merb-auth-slice-password-reset/mixins/senile_user/mm_senile_user.rb +22 -0
  22. data/lib/merb-auth-slice-password-reset/mixins/senile_user/sq_senile_user.rb +20 -0
  23. data/lib/merb-auth-slice-password-reset/slicetasks.rb +18 -0
  24. data/lib/merb-auth-slice-password-reset/spectasks.rb +75 -0
  25. data/public/javascripts/master.js +0 -0
  26. data/public/stylesheets/master.css +2 -0
  27. data/spec/mailers/password_reset_mailer_spec.rb +50 -0
  28. data/spec/mixins/senile_user_spec.rb +111 -0
  29. data/spec/requests/passwords_spec.rb +85 -0
  30. data/spec/spec_helper.rb +61 -0
  31. data/stubs/app/controllers/passwords.rb +13 -0
  32. data/stubs/app/mailers/views/password_reset_mailer/new_password.html.erb +3 -0
  33. data/stubs/app/mailers/views/password_reset_mailer/password_reset.text.erb +5 -0
  34. data/stubs/app/views/passwords/forgot_password.html.erb +7 -0
  35. metadata +169 -0
@@ -0,0 +1,13 @@
1
+ class MerbAuthSlicePasswordReset::PasswordResetMailer < Merb::MailController
2
+
3
+ include Merb::MerbAuthSlicePasswordReset::MailerHelper
4
+
5
+ controller_for_slice MerbAuthSlicePasswordReset, :templates_for => :mailer, :path => "views"
6
+
7
+ def password_reset
8
+ @user = params[:user]
9
+ Merb.logger.info "Sending Password Reset to #{@user.email} with code #{@user.password_reset_code}"
10
+ render_mail :layout => nil
11
+ end
12
+
13
+ end
@@ -0,0 +1,3 @@
1
+ To reset your password, please visit:
2
+
3
+ <%= password_reset_url(@user) %>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-us" lang="en-us">
3
+ <head>
4
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
5
+ <title>Fresh MerbAuthSlicePasswordReset Slice</title>
6
+ <link href="<%= public_path_for :stylesheet, 'master.css' %>" type="text/css" charset="utf-8" rel="stylesheet" media="all" />
7
+ <script src="<%= public_path_for :javascript, 'master.js' %>" type="text/javascript" charset="utf-8"></script>
8
+ </head>
9
+ <!-- you can override this layout at slices/merb-auth-slice-password-reset/app/views/layout/merb-auth-slice-password-reset.html.erb -->
10
+ <body class="merb-auth-slice-password-reset-slice">
11
+ <div id="container">
12
+ <h1>MerbAuthSlicePasswordReset Slice</h1>
13
+ <div id="main"><%= catch_content :for_layout %></div>
14
+ </div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,7 @@
1
+ <form action="" method="post">
2
+ <p>
3
+ <label for="<%= @login_param_name %>"><%= @login_param_name.to_s.capitalize.t %></label>
4
+ <input type="text" class="text" name="<%= @login_param_name %>" id="<%= @login_param_name %>" />
5
+ </p>
6
+ <input type="submit" value="Reset password" />
7
+ </form>
@@ -0,0 +1,15 @@
1
+ <h1>Please set a new password</h1>
2
+
3
+ <form action="<%= slice_url(:reset_check) %>" method="post">
4
+ <table>
5
+ <tr>
6
+ <td>Password</td>
7
+ <td><input type="password" name="user[password]" value="" id="user_password" /></td>
8
+ </tr>
9
+ <tr>
10
+ <td>Confirmation</td>
11
+ <td><input type="password" name="user[password_confirmation]" value="" id="user_password_confirmation" /></td>
12
+ </tr>
13
+ </table>
14
+ <input type="submit" value="Reset password" />
15
+ </form>
@@ -0,0 +1,80 @@
1
+ #
2
+ # ==== Standalone MerbAuthSlicePasswordReset configuration
3
+ #
4
+ # This configuration/environment file is only loaded by bin/slice, which can be
5
+ # used during development of the slice. It has no effect on this slice being
6
+ # loaded in a host application. To run your slice in standalone mode, just
7
+ # run 'slice' from its directory. The 'slice' command is very similar to
8
+ # the 'merb' command, and takes all the same options, including -i to drop
9
+ # into an irb session for example.
10
+ #
11
+ # The usual Merb configuration directives and init.rb setup methods apply,
12
+ # including use_orm and before_app_loads/after_app_loads.
13
+ #
14
+ # If you need need different configurations for different environments you can
15
+ # even create the specific environment file in config/environments/ just like
16
+ # in a regular Merb application.
17
+ #
18
+ # In fact, a slice is no different from a normal # Merb application - it only
19
+ # differs by the fact that seamlessly integrates into a so called 'host'
20
+ # application, which in turn can override or finetune the slice implementation
21
+ # code and views.
22
+ #
23
+
24
+ require(File.join(File.expand_path(File.dirname(__FILE__)),"..","lib","merb-auth-slice-password-reset"))
25
+
26
+ # Setup the required configuration for the slice
27
+ Merb::Slices::config[:merb_auth_slice_password_reset][:from_email] = "homer@example.com"
28
+ Merb::Slices::config[:merb_auth_slice_password_reset][:password_reset_host] = "example.com"
29
+
30
+
31
+ Merb::BootLoader.before_app_loads do
32
+ require "dm-core"
33
+ DataMapper.setup(:default, "sqlite3::memory:")
34
+ class User
35
+ include DataMapper::Resource
36
+ include Merb::Authentication::Mixins::SenileUser
37
+ property :id, Serial
38
+ property :email, String
39
+ property :login, String
40
+ property :password, String
41
+
42
+ def password_confirmation=(val); end;
43
+ end
44
+
45
+ class Merb::Authentication
46
+ def self.user_class
47
+ ::User
48
+ end
49
+
50
+ def store_user(user)
51
+ return nil if user.nil?
52
+ user.login
53
+ end
54
+ def fetch_user(user_id)
55
+ User.first(:login => login)
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ Merb::Config.use do |c|
62
+
63
+ # Sets up a custom session id key which is used for the session persistence
64
+ # cookie name. If not specified, defaults to '_session_id'.
65
+ # c[:session_id_key] = '_session_id'
66
+
67
+ # The session_secret_key is only required for the cookie session store.
68
+ c[:session_secret_key] = 'fe91f7d7dc909e8d5f59f8db8b4f6f866f358fae'
69
+
70
+ # There are various options here, by default Merb comes with 'cookie',
71
+ # 'memory', 'memcache' or 'container'.
72
+ # You can of course use your favorite ORM instead:
73
+ # 'datamapper', 'sequel' or 'activerecord'.
74
+ c[:session_store] = 'cookie'
75
+
76
+ # When running a slice standalone, you're usually developing it,
77
+ # so enable template reloading by default.
78
+ c[:reload_templates] = true
79
+
80
+ end
@@ -0,0 +1,82 @@
1
+ if defined?(Merb::Plugins)
2
+
3
+ $:.unshift File.dirname(__FILE__)
4
+
5
+ require(File.expand_path(File.dirname(__FILE__) / "merb-auth-slice-password-reset" / "mixins") / "senile_user")
6
+
7
+ Merb::Plugins.add_rakefiles "merb-auth-slice-password-reset/merbtasks", "merb-auth-slice-password-reset/slicetasks", "merb-auth-slice-password-reset/spectasks"
8
+
9
+ # Register the Slice for the current host application
10
+ Merb::Slices::register(__FILE__)
11
+
12
+ # Slice configuration - set this in a before_app_loads callback.
13
+ # By default a Slice uses its own layout, so you can swicht to
14
+ # the main application layout or no layout at all if needed.
15
+ #
16
+ # Configuration options:
17
+ # :layout - the layout to use; defaults to :merb-auth-slice-password-reset
18
+ # :mirror - which path component types to use on copy operations; defaults to all
19
+ Merb::Slices::config[:merb_auth_slice_password_reset][:layout] ||= :application
20
+
21
+ # All Slice code is expected to be namespaced inside a module
22
+ module MerbAuthSlicePasswordReset
23
+
24
+ # Slice metadata
25
+ self.description = "MerbAuthSlicePasswordReset is a merb slice that adds password-reset functionality for merb-auth-based merb applications."
26
+ self.version = "1.1.0"
27
+ self.author = "Daniel Neighman, Christian Kebekus"
28
+
29
+ # Stub classes loaded hook - runs before LoadClasses BootLoader
30
+ # right after a slice's classes have been loaded internally.
31
+ def self.loaded
32
+ end
33
+
34
+ # Initialization hook - runs before AfterAppLoads BootLoader
35
+ def self.init
36
+ end
37
+
38
+ # Activation hook - runs after AfterAppLoads BootLoader
39
+ def self.activate
40
+ end
41
+
42
+ # Deactivation hook - triggered by Merb::Slices.deactivate(MerbAuthSlicePasswordReset)
43
+ def self.deactivate
44
+ end
45
+
46
+ # Setup routes inside the host application
47
+ #
48
+ # @param scope<Merb::Router::Behaviour>
49
+ # Routes will be added within this scope (namespace). In fact, any
50
+ # router behaviour is a valid namespace, so you can attach
51
+ # routes at any level of your router setup.
52
+ #
53
+ # @note prefix your named routes with :merb_auth_slice_password_reset_
54
+ # to avoid potential conflicts with global named routes.
55
+ def self.setup_router(scope)
56
+ scope.match("/reset_password/:password_reset_code", :method => :get).to(:controller => "passwords", :action => "reset").name(:reset_password)
57
+ scope.match("/reset_check/:password_reset_code", :method => :post).to(:controller => "passwords", :action => "reset_check").name(:reset_check)
58
+ scope.match("/forgot_password", :method => :get).to(:controller => "passwords", :action => "forgot_password").name(:forgot_password)
59
+ scope.match("/forgot_password", :method => :post).to(:controller => "passwords", :action => "send_confirmation")
60
+ end
61
+
62
+ end
63
+
64
+ # Setup the slice layout for MerbAuthSlicePasswordReset
65
+ #
66
+ # Use MerbAuthSlicePasswordReset.push_path and MerbAuthSlicePasswordReset.push_app_path
67
+ # to set paths to merb-auth-slice-password-reset-level and app-level paths. Example:
68
+ #
69
+ # MerbAuthSlicePasswordReset.push_path(:application, MerbAuthSlicePasswordReset.root)
70
+ # MerbAuthSlicePasswordReset.push_app_path(:application, Merb.root / 'slices' / 'merb-auth-slice-password-reset')
71
+ # ...
72
+ #
73
+ # Any component path that hasn't been set will default to MerbAuthSlicePasswordReset.root
74
+ #
75
+ # Or just call setup_default_structure! to setup a basic Merb MVC structure.
76
+ MerbAuthSlicePasswordReset.setup_default_structure!
77
+ MaSPR = MerbAuthSlicePasswordReset unless defined?(MaSPR)
78
+
79
+ # Add dependencies for other MerbAuthSlicePasswordReset classes below. Example:
80
+ # dependency "merb-auth-slice-password-reset/other"
81
+
82
+ end
@@ -0,0 +1,112 @@
1
+ namespace :slices do
2
+ namespace :"merb-auth-slice-password-reset" do
3
+
4
+ desc "Install MerbAuthSlicePasswordReset"
5
+ task :install => [:preflight, :setup_directories, :copy_assets, :migrate]
6
+
7
+ desc "Test for any dependencies"
8
+ task :preflight do # see slicetasks.rb
9
+ end
10
+
11
+ desc "Setup directories"
12
+ task :setup_directories do
13
+ puts "Creating directories for host application"
14
+ MerbAuthSlicePasswordReset.mirrored_components.each do |type|
15
+ if File.directory?(MerbAuthSlicePasswordReset.dir_for(type))
16
+ if !File.directory?(dst_path = MerbAuthSlicePasswordReset.app_dir_for(type))
17
+ relative_path = dst_path.relative_path_from(Merb.root)
18
+ puts "- creating directory :#{type} #{File.basename(Merb.root) / relative_path}"
19
+ mkdir_p(dst_path)
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ desc "Copy stub files to host application"
26
+ task :stubs do
27
+ puts "Copying stubs for MerbAuthSlicePasswordReset - resolves any collisions"
28
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_stubs!
29
+ puts "- no files to copy" if copied.empty? && preserved.empty?
30
+ copied.each { |f| puts "- copied #{f}" }
31
+ preserved.each { |f| puts "! preserved override as #{f}" }
32
+ end
33
+
34
+ desc "Copy stub files and views to host application"
35
+ task :patch => [ "stubs", "freeze:views" ]
36
+
37
+ desc "Copy public assets to host application"
38
+ task :copy_assets do
39
+ puts "Copying assets for MerbAuthSlicePasswordReset - resolves any collisions"
40
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_public!
41
+ puts "- no files to copy" if copied.empty? && preserved.empty?
42
+ copied.each { |f| puts "- copied #{f}" }
43
+ preserved.each { |f| puts "! preserved override as #{f}" }
44
+ end
45
+
46
+ desc "Migrate the database"
47
+ task :migrate do # see slicetasks.rb
48
+ end
49
+
50
+ desc "Freeze MerbAuthSlicePasswordReset into your app (only merb-auth-slice-password-reset/app)"
51
+ task :freeze => [ "freeze:app" ]
52
+
53
+ namespace :freeze do
54
+
55
+ desc "Freezes MerbAuthSlicePasswordReset by installing the gem into application/gems"
56
+ task :gem do
57
+ ENV["GEM"] ||= "merb-auth-slice-password-reset"
58
+ Rake::Task['slices:install_as_gem'].invoke
59
+ end
60
+
61
+ desc "Freezes MerbAuthSlicePasswordReset by copying all files from merb-auth-slice-password-reset/app to your application"
62
+ task :app do
63
+ puts "Copying all merb-auth-slice-password-reset/app files to your application - resolves any collisions"
64
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_app!
65
+ puts "- no files to copy" if copied.empty? && preserved.empty?
66
+ copied.each { |f| puts "- copied #{f}" }
67
+ preserved.each { |f| puts "! preserved override as #{f}" }
68
+ end
69
+
70
+ desc "Freeze all views into your application for easy modification"
71
+ task :views do
72
+ puts "Copying all view templates to your application - resolves any collisions"
73
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_files_for :view
74
+ puts "- no files to copy" if copied.empty? && preserved.empty?
75
+ copied.each { |f| puts "- copied #{f}" }
76
+ preserved.each { |f| puts "! preserved override as #{f}" }
77
+ end
78
+
79
+ desc "Freeze all mailers into your application for easy modification"
80
+ task :mailers do
81
+ puts "Copying all mailer templates to your application - resolves any collisions"
82
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_files_for :mailer
83
+ puts "- no files to copy" if copied.empty? && preserved.empty?
84
+ copied.each { |f| puts "- copied #{f}" }
85
+ preserved.each { |f| puts "! preserved override as #{f}" }
86
+ end
87
+
88
+ desc "Freeze all models into your application for easy modification"
89
+ task :models do
90
+ puts "Copying all models to your application - resolves any collisions"
91
+ copied, preserved = MerbAuthSlicePasswordReset.mirror_files_for :model
92
+ puts "- no files to copy" if copied.empty? && preserved.empty?
93
+ copied.each { |f| puts "- copied #{f}" }
94
+ preserved.each { |f| puts "! preserved override as #{f}" }
95
+ end
96
+
97
+ desc "Freezes MerbAuthSlicePasswordReset as a gem and copies over merb-auth-slice-password-reset/app"
98
+ task :app_with_gem => [:gem, :app]
99
+
100
+ desc "Freezes MerbAuthSlicePasswordReset by unpacking all files into your application"
101
+ task :unpack do
102
+ puts "Unpacking MerbAuthSlicePasswordReset files to your application - resolves any collisions"
103
+ copied, preserved = MerbAuthSlicePasswordReset.unpack_slice!
104
+ puts "- no files to copy" if copied.empty? && preserved.empty?
105
+ copied.each { |f| puts "- copied #{f}" }
106
+ preserved.each { |f| puts "! preserved override as #{f}" }
107
+ end
108
+
109
+ end
110
+
111
+ end
112
+ end
@@ -0,0 +1,81 @@
1
+ module Merb
2
+ class Authentication
3
+ module Mixins
4
+ # This mixin provides basic password-reset functionality for senile users.
5
+ #
6
+ # Added properties:
7
+ # :password_reset_code, String
8
+ #
9
+ # To use it simply require it and include it into your user class.
10
+ #
11
+ # class User
12
+ # include Merb::Authentication::Mixins::SenileUser
13
+ #
14
+ # end
15
+ #
16
+ module SenileUser
17
+ def self.included(base)
18
+ base.class_eval do
19
+ include Merb::Authentication::Mixins::SenileUser::InstanceMethods
20
+ extend Merb::Authentication::Mixins::SenileUser::ClassMethods
21
+
22
+ path = File.expand_path(File.dirname(__FILE__)) / "senile_user"
23
+ if defined?(DataMapper) && DataMapper::Resource > self
24
+ require path / "dm_senile_user"
25
+ extend(Merb::Authentication::Mixins::SenileUser::DMClassMethods)
26
+ elsif defined?(ActiveRecord) && ancestors.include?(ActiveRecord::Base)
27
+ require path / "ar_senile_user"
28
+ extend(Merb::Authentication::Mixins::SenileUser::ARClassMethods)
29
+ elsif defined?(Sequel) && ancestors.include?(Sequel::Model)
30
+ require path / "sq_senile_user"
31
+ extend(Merb::Authentication::Mixins::SenileUser::SQClassMethods)
32
+ elsif MongoMapper::Document > self
33
+ require path / "mm_senile_user"
34
+ extend(Merb::Authentication::Mixins::SenileUser::MMClassMethods)
35
+ end
36
+
37
+ end # base.class_eval
38
+ end # self.included
39
+
40
+ module ClassMethods
41
+ def make_key
42
+ Digest::SHA1.hexdigest( Time.now.to_s.split(//).sort_by {rand}.join )
43
+ end
44
+ end # ClassMethods
45
+
46
+ module InstanceMethods
47
+ def generate_password_reset_code
48
+ pwreset_key_success = false
49
+ until pwreset_key_success
50
+ self.password_reset_code = self.class.make_key
51
+ respond_to?(:save!) ? save! : save
52
+ pwreset_key_success = self.errors.on(:password_reset_code).nil? ? true : false
53
+ end
54
+ end
55
+
56
+ def password_reset?
57
+ ! self.password_reset_code.nil?
58
+ end
59
+
60
+ # Sends out the password reset notification.
61
+ # Used 'Request to change your password' as subject if +MaSFP[:password_reset_subject]+ is not set.
62
+ def send_password_reset_notification
63
+ generate_password_reset_code
64
+ deliver_password_reset_email(:password_reset, :subject => (MaSPR[:password_reset_subject] || "Request to change your password"))
65
+ end
66
+
67
+ private
68
+
69
+ # Helper method delivering the email.
70
+ def deliver_password_reset_email(action, params)
71
+ from = MaSPR[:from_email]
72
+ raise "No :from_email option set for Merb::Slices::config[:merb_auth_slice_password_reset][:from_email]" unless from
73
+ MaSPR::PasswordResetMailer.dispatch_and_deliver(action, params.merge(:from => from, :to => self.email), :user => self)
74
+ end
75
+
76
+ end # InstanceMethods
77
+
78
+ end # SenileUser
79
+ end # Mixins
80
+ end # Authentication
81
+ end # Merb
@@ -0,0 +1,19 @@
1
+ module Merb
2
+ class Authentication
3
+ module Mixins
4
+ module SenileUser
5
+ module ARClassMethods
6
+ def self.extended(base)
7
+ def find_with_password_reset_code(code)
8
+ find(:first, :conditions => ["password_reset_code = ?", code])
9
+ end
10
+
11
+ def find_with_login_param(param_name, value)
12
+ find(:first, :conditions => ["#{param_name} = ?", value])
13
+ end
14
+ end # self.extended
15
+ end # ARClassMethods
16
+ end # SenileUser
17
+ end # Mixins
18
+ end # Authentication
19
+ end # Merb
@@ -0,0 +1,22 @@
1
+ module Merb
2
+ class Authentication
3
+ module Mixins
4
+ module SenileUser
5
+ module DMClassMethods
6
+ def self.extended(base)
7
+ base.class_eval do
8
+ property :password_reset_code, String, :writer => :protected
9
+ end # base.class_eval
10
+ def find_with_password_reset_code(code)
11
+ first(:password_reset_code => code)
12
+ end
13
+
14
+ def find_with_login_param(param_name, value)
15
+ first(param_name => value)
16
+ end
17
+ end # self.extended
18
+ end # DMClassMethods
19
+ end # SenileUser
20
+ end # Mixins
21
+ end # Authentication
22
+ end #Merb