merb_auth_slice_multisite 0.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/README.textile +147 -0
  2. data/VERSION.yml +4 -0
  3. data/app/controllers/application.rb +5 -0
  4. data/app/controllers/exceptions.rb +33 -0
  5. data/app/controllers/passwords.rb +29 -0
  6. data/app/controllers/sessions.rb +56 -0
  7. data/app/helpers/application_helper.rb +64 -0
  8. data/app/mailers/send_password_mailer.rb +11 -0
  9. data/app/mailers/views/send_password_mailer/send_password.text.erb +3 -0
  10. data/app/models/site.rb +26 -0
  11. data/app/views/exceptions/unauthenticated.html.erb +61 -0
  12. data/app/views/layout/merb_auth_slice_multisite.html.erb +16 -0
  13. data/config/database.yml +33 -0
  14. data/config/dependencies.rb +33 -0
  15. data/config/init.rb +84 -0
  16. data/config/router.rb +5 -0
  17. data/lib/merb-auth-more/strategies/multisite/multisite_password_form.rb +77 -0
  18. data/lib/merb-auth-remember-me/mixins/authenticated_user.rb +97 -0
  19. data/lib/merb-auth-remember-me/mixins/authenticated_user/dm_authenticated_user.rb +17 -0
  20. data/lib/merb-auth-remember-me/strategies/remember_me.rb +55 -0
  21. data/lib/merb_auth_slice_multisite.rb +107 -0
  22. data/lib/merb_auth_slice_multisite/merbtasks.rb +103 -0
  23. data/lib/merb_auth_slice_multisite/mixins/user_belongs_to_site.rb +63 -0
  24. data/lib/merb_auth_slice_multisite/mixins/user_belongs_to_site/dm_user_belongs_to_site.rb +28 -0
  25. data/lib/merb_auth_slice_multisite/slicetasks.rb +18 -0
  26. data/lib/merb_auth_slice_multisite/spectasks.rb +54 -0
  27. data/public/javascripts/master.js +0 -0
  28. data/public/stylesheets/master.css +2 -0
  29. data/spec/mailers/send_password_mailer_spec.rb +47 -0
  30. data/spec/mixins/authenticated_user_spec.rb +33 -0
  31. data/spec/mixins/user_belongs_to_site_spec.rb +56 -0
  32. data/spec/models/site_spec.rb +56 -0
  33. data/spec/spec_helper.rb +101 -0
  34. data/spec/strategies/remember_me_spec.rb +62 -0
  35. data/stubs/app/controllers/sessions.rb +19 -0
  36. data/stubs/app/views/exceptions/unauthenticated.html.erb +61 -0
  37. metadata +91 -0
@@ -0,0 +1,147 @@
1
+ h1. MerbAuthSliceMultisite
2
+
3
+ This slice setups multisite capabilities in your merb application with subdomains (i.e. coolcars.yourapp.com) and an authentication check.
4
+
5
+ Noteworthy: This multisite gem validates the usernames by the scope of the subdomain. This means someone can create another subdomain site using the same username and password, but technically it will be a new user in the users table - similar to how blinksale works. This is how I prefer things. It saves the hassle of a has_and_belongs_to_many relationship and the extra signup hoops you have to go through as a user if you want to create multiple accounts - lighthouse is an example of this.
6
+
7
+ h2. Instructions for installation:
8
+
9
+ 1. Add github as a gem source & install
10
+ <pre><code>gem sources -a http://gems.github.com
11
+ sudo gem install scottmotte-merb_auth_slice_multisite
12
+ </code></pre>
13
+
14
+ 2. Setup your application to use the gem.* Add the following to the end of dependencies.rb.
15
+ <pre><code>dependency "scottmotte-merb_auth_slice_multisite", :require_as => 'merb_auth_slice_multisite'</code></pre>
16
+
17
+ 3. Remove default merb-auth-slice-password in dependencies.rb
18
+ <pre></code><strike>dependency "merb-auth-slice-password"</strike></code></pre>
19
+ (merb_auth_slice_multisite is now standalone. it does not depend on merb-auth-slice-password)
20
+
21
+ 4. Add in mixin. In your user model or in merb/merb-auth/setup.rb add the mixin
22
+ include Merb::Authentication::Mixins::UserBelongsToSite and then migrate your database.
23
+ <pre><code>
24
+ # in model
25
+ class User
26
+ include Merb::Authentication::Mixins::UserBelongsToSite
27
+ include Merb::Authentication::Mixins::AuthenticatedUser #for remember me functionality
28
+ end
29
+
30
+
31
+ # or as I prefer in merb/merb-auth/setup.rb
32
+ Merb::Authentication.user_class.class_eval{
33
+ include Merb::Authentication::Mixins::SaltedUser
34
+ include Merb::Authentication::Mixins::ActivatedUser
35
+ include Merb::Authentication::Mixins::UserBelongsToSite # <-- this one
36
+ include Merb::Authentication::Mixins::AuthenticatedUser # <-- and this one
37
+ }
38
+ </code></pre>
39
+
40
+ _Don't forget to migrate your database schema with rake db:autoupgrade or rake db:automigrate_
41
+
42
+ 5. Setup strategies.rb - make sure it looks like the following. merb_auth_slice_multisite uses its own custom strategy so including the others will mess things up.
43
+ <pre><code>Merb::Slices::config[:"merb_auth_slice_multisite"][:no_default_strategies] = false</code></pre>
44
+
45
+ As an aside, here's what an example router might look like using the slice to support subdomains. See how it checks for subdomain equaling www or nil and routes to the main site. And then everything else goes to the pages controller or users controller that has a subdomain. All of the controller actions then user @current_site.pages.all and @current_site.users.all instead of just User.all & Page.all. Starting to come together in your mind :)
46
+
47
+ <pre><code>
48
+ Merb.logger.info("Compiling routes...")
49
+ Merb::Router.prepare do
50
+
51
+ # Brochure site - equal to site_url from settings.yml and has no first_subdomain else do below
52
+ match(:first_subdomain => /^(www)*$/) do
53
+ resources :sites, :collection => { :changed_on_spreedly => :post }
54
+ resources :static
55
+ match('/signup').to(:controller => 'sites', :action => 'new')
56
+ match('/').to(:controller => 'static', :action => 'index')
57
+ end
58
+
59
+ # Subdomain sites
60
+ resources :pages
61
+ resources :users, :identify => :login
62
+
63
+ match('/').to(:controller => 'pages', :action => 'index')
64
+ slice(:merb_auth_slice_multisite, :name_prefix => nil, :path_prefix => "") # /login, /logout
65
+ end
66
+ </pre></code>
67
+
68
+ 6. Add the slice's routes to your router
69
+ <pre><code>slice(:merb_auth_slice_multisite, :name_prefix => nil, :path_prefix => "")</code></pre>
70
+
71
+ 7. Setup @current_site value. I haven't worked out how to make it automatically accessible yet so for now paste the following into your app's application.rb file.
72
+ <pre><code>
73
+ before :get_site
74
+ def get_site
75
+ # uses @current_site - for example, to create pages under current site do @current_site.pages.new
76
+ @current_site = Site.first(:subdomain => request.first_subdomain)
77
+ raise NotFound unless @current_site
78
+ end
79
+ </code></pre>
80
+
81
+ 8. Configure forgot_password functionality. Add the following into merb/merb-auth/setup.rb
82
+ <pre><code># To change the parameter names for the password or login field you may set either of these two options
83
+ #
84
+ # Merb::Plugins.config[:"merb-auth"][:login_param] = :email
85
+ # Merb::Plugins.config[:"merb-auth"][:password_param] = :my_password_field_name
86
+ Merb::Slices::config[:merb_auth_slice_multisite][:send_password_from_email] = "no-reply@yourapp.com"
87
+ Merb::Slices::config[:merb_auth_slice_multisite][:domain] = "example.com"</code></pre>
88
+
89
+
90
+ h2. Additional details:
91
+
92
+ Schema/Migrations. The mixin adds some fields to your user model. Where needed include these in your migrations if you are using migrations.
93
+ <pre><code># Relationships/Associations
94
+ belongs_to :site
95
+ property :site_id, Integer
96
+ validates_is_unique :login, :scope => :site_id
97
+ validates_is_unique :email, :scope => :site_id
98
+ </code></pre>
99
+
100
+ Site model. You're probably wondering where the heck is the site model. It's in the slice. You can override it by running one of the rake tasks or you can create your own site.rb model and add additional fields which is what I do. For example, if you have pages under a site, you might do something like:
101
+ <pre><code># site.rb
102
+ class Site
103
+ has n, :pages, :order => [:position.asc]
104
+ end
105
+ </code></pre>
106
+
107
+ @current_site. You can use @current_site in your controllers like so:
108
+ <pre><code>
109
+ class Pages < Application
110
+ # provides :xml, :yaml, :js
111
+
112
+ def index
113
+ @pages = @current_site.pages.all
114
+ display @pages
115
+ end
116
+
117
+ def show(id)
118
+ @page = @current_site.pages.get(id)
119
+ raise NotFound unless @page
120
+ display @page
121
+ end
122
+
123
+ end # Pages
124
+ </pre></code>
125
+
126
+
127
+ h2. Assumptions
128
+
129
+ * works for subdomains (i.e. coolcars.yourapp.com)
130
+ * merb only
131
+ * merb-auth-core dependency
132
+ * merb-auth-more dependency
133
+ * *only supports datamapper so far* (help me extend it! fork the project and request me to pull)
134
+ * A site has n (has_many) users. A user belongs_to a site.
135
+ * You can have multiple users with the same username and password as long as they each belong_to a different site. For example, there can be an admin user with the credentials { :login => 'admin', :email => 'admin@example.org', :site_id => 1} and an admin user with the credentials { :login => 'admin', :email => 'admin@example.org', :site_id => 66}. As long as the site_id is different then it is ok. This allows more freedom when your users want to setup multiple sites.
136
+
137
+
138
+ h2. How does it work?
139
+
140
+ When logging in the "user" object found by merb-auth-core will be asked whether the user's login, password, and site_id match. It matches the site_id against the @current_site.id, and the login/password from the standard form fields.
141
+
142
+
143
+ h2. Rake tasks
144
+
145
+ To see all available tasks for MerbAuthSliceMultisite run:
146
+
147
+ rake -T slices:merb_auth_slice_multisite
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 8
4
+ :patch: 6
@@ -0,0 +1,5 @@
1
+ class MerbAuthSliceMultisite::Application < Merb::Controller
2
+
3
+ controller_for_slice
4
+
5
+ end
@@ -0,0 +1,33 @@
1
+ # the mixin to provide the exceptions controller action for Unauthenticated
2
+ module MerbAuthSliceMultisite::ExceptionsMixin
3
+ def unauthenticated
4
+ provides :xml, :js, :json, :yaml
5
+
6
+ case content_type
7
+ when :html
8
+ render
9
+ else
10
+ basic_authentication.request!
11
+ ""
12
+ end
13
+ end # unauthenticated
14
+ end
15
+
16
+ Merb::Authentication.customize_default do
17
+
18
+ Exceptions.class_eval do
19
+ include Merb::Slices::Support # Required to provide slice_url
20
+
21
+ # # This stuff allows us to provide a default view
22
+ the_view_path = File.expand_path(File.dirname(__FILE__) / ".." / "views")
23
+ self._template_roots ||= []
24
+ self._template_roots << [the_view_path, :_template_location]
25
+ self._template_roots << [Merb.dir_for(:view), :_template_location]
26
+
27
+ include MerbAuthSliceMultisite::ExceptionsMixin
28
+
29
+ show_action :unauthenticated
30
+
31
+ end# Exceptions.class_eval
32
+
33
+ end # Customize default
@@ -0,0 +1,29 @@
1
+ class MerbAuthSliceMultisite::Passwords < MerbAuthSliceMultisite::Application
2
+
3
+ def send_password
4
+ @login_param = Merb::Authentication::Strategies::Multisite::Base.login_param
5
+ @site_id_param = Merb::Authentication::Strategies::Multisite::Base.site_id_param
6
+ @user = Merb::Authentication.user_class.first(@login_param => params[@login_param], @site_id_param => params[@site_id_param])
7
+
8
+ if @user
9
+ from = MerbAuthSliceMultisite[:send_password_from_email]
10
+ raise "No :send_password_from_email option set for Merb::Slices::config[:merb_auth_slice_multisite][:send_password_from_email]" unless from
11
+ @user.password = @user.password_confirmation = new_generated_password
12
+ send_mail(MerbAuthSliceMultisite::SendPasswordMailer, :send_password, { :subject => (MerbAuthSliceMultisite[:send_password_subject] || "Forgetful? :)"), :from => from, :to => @user.email }, { :user => @user })
13
+ @user.save
14
+ redirect "/", :message => {:notice => "Password sent. Check your email."}
15
+ else
16
+ redirect "/", :message => {:error => "User with #{@login_param} \"%s\" not found".t(params[@login_param].freeze)}
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def new_generated_password
23
+ chars = ("a".."z").to_a
24
+ start_password = ""
25
+ 1.upto(6) { |i| start_password << chars[rand(chars.size-1)] }
26
+ @password = start_password
27
+ end
28
+
29
+ end # MerbAuthSliceMultisite::Passwords
@@ -0,0 +1,56 @@
1
+ class MerbAuthSliceMultisite::Sessions < MerbAuthSliceMultisite::Application
2
+
3
+ before :_maintain_auth_session_before, :exclude => [:destroy] # Need to hang onto the redirection during the session.abandon!
4
+ before :_abandon_session, :only => [:update, :destroy]
5
+ before :_maintain_auth_session_after, :exclude => [:destroy] # Need to hang onto the redirection during the session.abandon!
6
+ before :ensure_authenticated, :only => [:update]
7
+
8
+ # redirect from an after filter for max flexibility
9
+ # We can then put it into a slice and ppl can easily
10
+ # customize the action
11
+ after :redirect_after_login, :only => :update, :if => lambda{ !(300..399).include?(status) }
12
+ after :redirect_after_logout, :only => :destroy
13
+
14
+ def update
15
+ "Add an after filter to do stuff after login"
16
+ end
17
+
18
+ def destroy
19
+ "Add an after filter to do stuff after logout"
20
+ cookies.delete :auth_token
21
+ end
22
+
23
+
24
+ private
25
+ # @overwritable
26
+ def redirect_after_login
27
+ message[:notice] = "Authenticated Successfully"
28
+ redirect_back_or "/", :message => message, :ignore => [slice_url(:login), slice_url(:logout)]
29
+ end
30
+
31
+ # @overwritable
32
+ def redirect_after_logout
33
+ message[:notice] = "Logged Out"
34
+ redirect "/", :message => message
35
+ end
36
+
37
+ # @private
38
+ def _maintain_auth_session_before
39
+ @_maintain_auth_session = {}
40
+ Merb::Authentication.maintain_session_keys.each do |k|
41
+ @_maintain_auth_session[k] = session[k]
42
+ end
43
+ end
44
+
45
+ # @private
46
+ def _maintain_auth_session_after
47
+ @_maintain_auth_session.each do |k,v|
48
+ session[k] = v
49
+ end
50
+ end
51
+
52
+ # @private
53
+ def _abandon_session
54
+ session.abandon!
55
+ end
56
+ end
@@ -0,0 +1,64 @@
1
+ module Merb
2
+ module MerbAuthSliceMultisite
3
+ module ApplicationHelper
4
+
5
+ # @param *segments<Array[#to_s]> Path segments to append.
6
+ #
7
+ # @return <String>
8
+ # A path relative to the public directory, with added segments.
9
+ def image_path(*segments)
10
+ public_path_for(:image, *segments)
11
+ end
12
+
13
+ # @param *segments<Array[#to_s]> Path segments to append.
14
+ #
15
+ # @return <String>
16
+ # A path relative to the public directory, with added segments.
17
+ def javascript_path(*segments)
18
+ public_path_for(:javascript, *segments)
19
+ end
20
+
21
+ # @param *segments<Array[#to_s]> Path segments to append.
22
+ #
23
+ # @return <String>
24
+ # A path relative to the public directory, with added segments.
25
+ def stylesheet_path(*segments)
26
+ public_path_for(:stylesheet, *segments)
27
+ end
28
+
29
+ # Construct a path relative to the public directory
30
+ #
31
+ # @param <Symbol> The type of component.
32
+ # @param *segments<Array[#to_s]> Path segments to append.
33
+ #
34
+ # @return <String>
35
+ # A path relative to the public directory, with added segments.
36
+ def public_path_for(type, *segments)
37
+ ::MerbAuthSliceMultisite.public_path_for(type, *segments)
38
+ end
39
+
40
+ # Construct an app-level path.
41
+ #
42
+ # @param <Symbol> The type of component.
43
+ # @param *segments<Array[#to_s]> Path segments to append.
44
+ #
45
+ # @return <String>
46
+ # A path within the host application, with added segments.
47
+ def app_path_for(type, *segments)
48
+ ::MerbAuthSliceMultisite.app_path_for(type, *segments)
49
+ end
50
+
51
+ # Construct a slice-level path.
52
+ #
53
+ # @param <Symbol> The type of component.
54
+ # @param *segments<Array[#to_s]> Path segments to append.
55
+ #
56
+ # @return <String>
57
+ # A path within the slice source (Gem), with added segments.
58
+ def slice_path_for(type, *segments)
59
+ ::MerbAuthSliceMultisite.slice_path_for(type, *segments)
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,11 @@
1
+ class MerbAuthSliceMultisite::SendPasswordMailer < Merb::MailController
2
+
3
+ controller_for_slice MerbAuthSliceMultisite, :templates_for => :mailer, :path => "views"
4
+
5
+ def send_password
6
+ @user = params[:user]
7
+ Merb.logger.info "Sending Password to #{@user.email}"
8
+ render_mail :layout => nil
9
+ end
10
+
11
+ end
@@ -0,0 +1,3 @@
1
+ Your new password is <%= @user.password %>
2
+
3
+ Log on at http://<%= @user.site.subdomain %>.<%= Merb::Slices::config[:merb_auth_slice_multisite][:domain] %>
@@ -0,0 +1,26 @@
1
+ class Site
2
+ include DataMapper::Resource
3
+ include DataMapper::Timestamp
4
+
5
+ # Schema
6
+ property :id, Serial
7
+ property :subdomain, String, :nullable => false, :length => (1..40), :unique => true, :format => /^[a-zA-Z0-9\-]*?$/
8
+ property :created_at, DateTime
9
+ property :updated_at, DateTime
10
+
11
+ # Relationships/Associates
12
+ has n, :users, :order => [:login.asc]
13
+
14
+ # Validations
15
+ validates_with_method :check_subdomain
16
+
17
+ ReservedSubdomains = %w[backstage admin blog dev ftp mail email pop pop3 imap smtp stage stats status www]
18
+ def check_subdomain
19
+ if ReservedSubdomains.include?(self.subdomain)
20
+ [false, "Subdomain '#{self.subdomain}' is reserved."]
21
+ else
22
+ true
23
+ end
24
+ end
25
+
26
+ end
@@ -0,0 +1,61 @@
1
+ <% @login_param = Merb::Authentication::Strategies::Multisite::Base.login_param %>
2
+ <% @password_param = Merb::Authentication::Strategies::Multisite::Base.password_param %>
3
+ <% @site_id_param = Merb::Authentication::Strategies::Multisite::Base.site_id_param %>
4
+ <%
5
+ # make @current_site value. application.rb does not get call
6
+ # because the authentication is protected at the rack level - which is better,
7
+ # but it means I have to add the following duplicate line of code as far as I know.
8
+ @current_site = Site.first(:subdomain => request.first_subdomain)
9
+ %>
10
+
11
+ <%= error_messages_for session.authentication %>
12
+ <form action="<%= slice_url(:merb_auth_slice_multisite, :perform_login) %>" method="post" id="loginForm">
13
+ <h3>Login</h3>
14
+ <input type="hidden" name="<%= @site_id_param.to_s %>" value="<%= @current_site.id %>" id="<%= @site_id_param.to_s %>">
15
+ <input type="hidden" name="_method" value="PUT" />
16
+
17
+ <div id="loginFields" class="fields">
18
+ <div id="loginElements" class="elements">
19
+ <label for="<%= @login_param.to_s %>"><%= @login_param.to_s.capitalize %>:</label>
20
+ <div id="loginElement" class="element">
21
+ <input type="text" name="<%= @login_param.to_s %>" value="" id="<%= @login_param.to_s %>">
22
+ </div>
23
+ </div>
24
+ <div id="passwordElements" class="elements">
25
+ <label for="<%= @password_param.to_s %>"><%= @password_param.to_s.capitalize %>:</label>
26
+ <div id="passwordElement" class="element">
27
+ <input type="password" name="<%= @password_param.to_s %>" value="" id="<%= @password_param.to_s %>">
28
+ </div>
29
+ </div>
30
+ <div id="remember_meElements" class="elements">
31
+ <div id="remember_meElement" class="element">
32
+ <input type="checkbox" name="remember_me" value="1" id="remember_me" /> <label for="remember_me">Remember me</label>
33
+ </div>
34
+ </div>
35
+ <div id="loginActions" class="actions">
36
+ <div id="loginAction" class="action">
37
+ <input type="submit" name="loginSubmit" value="Log In" id="loginSubmit" />
38
+ </div>
39
+ <div id="forgotToggleAction" class="action">
40
+ <a href="#" id="forgotToggle">Forgot password</a>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </form>
45
+
46
+ <form action="<%= slice_url(:merb_auth_slice_multisite, :send_password) %>" method="post" id="forgotForm">
47
+ <input type="hidden" name="<%= @site_id_param.to_s %>" value="<%= @current_site.id %>" id="<%= @site_id_param.to_s %>">
48
+ <div id="forgotFields" class="fields">
49
+ <div id="forgotLoginElements" class="elements">
50
+ <label for="<%= @login_param.to_s %>"><%= @login_param.to_s.capitalize %>:</label>
51
+ <div id="forgotLoginElement" class="element">
52
+ <input type="text" class="text" name="<%= @login_param.to_s %>" id="<%= @login_param.to_s %>" />
53
+ </div>
54
+ </div>
55
+ <div id="forgotLoginActions" class="actions">
56
+ <div id="forgotLoginAction" class="action">
57
+ <input type="submit" name="forgotLoginSubmit" value="Reset Password" id="forgotLoginSubmit" />
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </form>