auth-slice 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +65 -0
- data/Rakefile +47 -0
- data/app/controllers/application.rb +4 -0
- data/app/controllers/controller_mixin.rb +150 -0
- data/app/controllers/users.rb +41 -0
- data/app/helpers/application_helper.rb +64 -0
- data/app/models/adapter/activerecord.rb +55 -0
- data/app/models/adapter/datamapper.rb +80 -0
- data/app/models/base_model.rb +76 -0
- data/app/views/layout/auth_slice.html.erb +47 -0
- data/app/views/users/login.html.erb +31 -0
- data/app/views/users/signup.html.erb +29 -0
- data/lib/auth-slice.rb +63 -0
- data/lib/auth-slice/merbtasks.rb +110 -0
- data/lib/auth-slice/model.rb +6 -0
- data/public/stylesheets/master.css +157 -0
- data/spec/auth-slice_spec.rb +52 -0
- data/spec/controllers/router_spec.rb +29 -0
- data/spec/controllers/session_spec.rb +87 -0
- data/spec/controllers/users_spec.rb +37 -0
- data/spec/controllers/view_helper_spec.rb +27 -0
- data/spec/models/ar_user_spec.rb +21 -0
- data/spec/models/dm_user_spec.rb +20 -0
- data/spec/models/shared_user_spec.rb +251 -0
- data/spec/spec_helper.rb +42 -0
- metadata +101 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 PragmaQuest Inc
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
AuthSlice
|
2
|
+
=========
|
3
|
+
|
4
|
+
An user authentication slice for the Merb framework. See http://github.com/ctran/merb-skel for an example application that uses this slice
|
5
|
+
|
6
|
+
== Installation:
|
7
|
+
|
8
|
+
> gem install merb
|
9
|
+
> gem install merb-slices
|
10
|
+
|
11
|
+
To use activerecord adapter
|
12
|
+
> gem install activerecord
|
13
|
+
> gem merb_activerecord
|
14
|
+
|
15
|
+
To use datamapper adapter
|
16
|
+
> gem install datamapper
|
17
|
+
> gem install merb_datamapper
|
18
|
+
|
19
|
+
=== config/init.rb
|
20
|
+
|
21
|
+
# add the slice as a regular dependency
|
22
|
+
dependency 'auth-slice'
|
23
|
+
|
24
|
+
=== config/router.rb
|
25
|
+
|
26
|
+
# This will add the following routes /login, /logout and /signup
|
27
|
+
r.slice(:AuthSlice)
|
28
|
+
|
29
|
+
# This will add the following routes /auth-slice/login, /auth-slice/logout and /auth-slice/signup
|
30
|
+
r.add_slice(:AuthSlice)
|
31
|
+
|
32
|
+
# This will add the following routes /user/login, /user/logout and /user/signup
|
33
|
+
r.add_slice(:AuthSlice, 'user') # same as :path => 'user'
|
34
|
+
|
35
|
+
=== Normally you should also run the following rake task:
|
36
|
+
> rake slices:auth_slice:install
|
37
|
+
|
38
|
+
== Customization/Overrides
|
39
|
+
|
40
|
+
By default, the user model class is in AuthSlice::User. To use shorter name, add this to your init.rb
|
41
|
+
|
42
|
+
Merb::Slices.config[:auth_slice] = { :user_model_class => "User" }
|
43
|
+
|
44
|
+
You can also put your application-level overrides in:
|
45
|
+
|
46
|
+
host-app/slices/auth-slice/app - controllers, models, views ...
|
47
|
+
|
48
|
+
Templates are located in this order:
|
49
|
+
|
50
|
+
1. host-app/slices/auth-slice/app/views/*
|
51
|
+
2. gems/auth-slice/app/views/*
|
52
|
+
3. host-app/app/views/*
|
53
|
+
|
54
|
+
You can use the host application's layout by configuring the
|
55
|
+
auth-slice slice in a before_app_loads block:
|
56
|
+
|
57
|
+
Merb::Slices.config[:auth_slice] = { :layout => :application }
|
58
|
+
|
59
|
+
By default :auth_slice is used. If you need to override
|
60
|
+
stylesheets or javascripts, just specify your own files in your layout
|
61
|
+
instead/in addition to the ones supplied (if any) in
|
62
|
+
host-app/public/slices/auth-slice.
|
63
|
+
|
64
|
+
In any case don't edit those files directly as they may be clobbered any time
|
65
|
+
rake auth_slice:install is run.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
require 'merb-core/version'
|
5
|
+
require 'merb-core/test/tasks/spectasks'
|
6
|
+
|
7
|
+
PLUGIN = "auth-slice"
|
8
|
+
NAME = "auth-slice"
|
9
|
+
AUTHOR = "Cuong Tran"
|
10
|
+
EMAIL = "ctran@pragmaquest.com"
|
11
|
+
HOMEPAGE = "http://merb-slices.rubyforge.org/auth-slice/"
|
12
|
+
SUMMARY = "Merb Slice that provides user authentication"
|
13
|
+
SLICE_VERSION = "0.1.0"
|
14
|
+
|
15
|
+
spec = Gem::Specification.new do |s|
|
16
|
+
s.name = NAME
|
17
|
+
s.version = SLICE_VERSION
|
18
|
+
s.platform = Gem::Platform::RUBY
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.extra_rdoc_files = ["README", "LICENSE"]
|
21
|
+
s.summary = SUMMARY
|
22
|
+
s.description = s.summary
|
23
|
+
s.author = AUTHOR
|
24
|
+
s.email = EMAIL
|
25
|
+
s.homepage = HOMEPAGE
|
26
|
+
s.add_dependency('merb-slices', '>= 0.9.4')
|
27
|
+
s.require_path = 'lib'
|
28
|
+
s.files = %w(LICENSE README Rakefile) + Dir.glob("{lib,spec,app,public}/**/*")
|
29
|
+
end
|
30
|
+
|
31
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
32
|
+
pkg.gem_spec = spec
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Install Foo as a gem"
|
36
|
+
task :install => [:package] do
|
37
|
+
sh %{sudo gem install pkg/#{NAME}-#{SLICE_VERSION} --no-update-sources --local}
|
38
|
+
end
|
39
|
+
|
40
|
+
namespace :jruby do
|
41
|
+
|
42
|
+
desc "Run :package and install the resulting .gem with jruby"
|
43
|
+
task :install => :package do
|
44
|
+
sh %{#{SUDO} jruby -S gem install pkg/#{NAME}-#{VERSION}.gem --no-rdoc --no-ri}
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module AuthSlice
|
2
|
+
module ControllerMixin
|
3
|
+
protected
|
4
|
+
# Returns true or false if the user is logged in.
|
5
|
+
# Preloads @current_user with the user model if they're logged in.
|
6
|
+
def logged_in?
|
7
|
+
current_user != :false
|
8
|
+
end
|
9
|
+
|
10
|
+
# Accesses the current user from the session. Set it to :false if login fails
|
11
|
+
# so that future calls do not hit the database.
|
12
|
+
def current_user
|
13
|
+
@current_user ||= (login_from_session || login_from_basic_auth || login_from_cookie || :false)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Store the given user in the session.
|
17
|
+
def current_user=(new_user)
|
18
|
+
session[:user] = (new_user.nil? || new_user.is_a?(Symbol)) ? nil : new_user.id
|
19
|
+
@current_user = new_user
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if the user is authorized
|
23
|
+
#
|
24
|
+
# Override this method in your controllers if you want to restrict access
|
25
|
+
# to only a few actions or if you want to check if the user
|
26
|
+
# has the correct rights.
|
27
|
+
#
|
28
|
+
# Example:
|
29
|
+
#
|
30
|
+
# # only allow nonbobs
|
31
|
+
# def authorized?
|
32
|
+
# current_user.username != "bob"
|
33
|
+
# end
|
34
|
+
def authorized?
|
35
|
+
logged_in?
|
36
|
+
end
|
37
|
+
|
38
|
+
# Filter method to enforce a login requirement.
|
39
|
+
#
|
40
|
+
# To require logins for all actions, use this in your controllers:
|
41
|
+
#
|
42
|
+
# before_filter :login_required
|
43
|
+
#
|
44
|
+
# To require logins for specific actions, use this in your controllers:
|
45
|
+
#
|
46
|
+
# before_filter :login_required, :only => [ :edit, :update ]
|
47
|
+
#
|
48
|
+
# To skip this in a subclassed controller:
|
49
|
+
#
|
50
|
+
# skip_before_filter :login_required
|
51
|
+
#
|
52
|
+
def login_required
|
53
|
+
authorized? || throw(:halt, :access_denied)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Redirect as appropriate when an access request fails.
|
57
|
+
#
|
58
|
+
# The default HTML action is to redirect to the login screen.
|
59
|
+
#
|
60
|
+
# The default XML action is to render the text Couldn't authenticate you.
|
61
|
+
# To provide this response wrapped in XML, make sure to specify an
|
62
|
+
# XML layout, such as /app/views/layouts/application.xml.builder.
|
63
|
+
#
|
64
|
+
# Override this method in your controllers if you want to have special
|
65
|
+
# behavior in case the user is not authorized
|
66
|
+
# to access the requested action. For example, a popup window might
|
67
|
+
# simply close itself.
|
68
|
+
def access_denied
|
69
|
+
case content_type
|
70
|
+
when :html
|
71
|
+
store_location
|
72
|
+
redirect url(:login)
|
73
|
+
when :xml
|
74
|
+
headers["Status"] = "Unauthorized"
|
75
|
+
headers["WWW-Authenticate"] = %(Basic realm="Web Password")
|
76
|
+
self.status = 401
|
77
|
+
render "Couldn't authenticate you"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Store the URI of the current request in the session.
|
82
|
+
#
|
83
|
+
# We can return to this location by calling #redirect_back_or_default.
|
84
|
+
def store_location
|
85
|
+
session[:return_to] = request.uri
|
86
|
+
end
|
87
|
+
|
88
|
+
# Redirect to the URI stored by the most recent store_location call or
|
89
|
+
# to the passed default.
|
90
|
+
def redirect_back_or_default(default)
|
91
|
+
loc = session[:return_to] || default
|
92
|
+
session[:return_to] = nil
|
93
|
+
redirect loc
|
94
|
+
end
|
95
|
+
|
96
|
+
# Inclusion hook to make #current_user and #logged_in?
|
97
|
+
# available as ActionView helper methods.
|
98
|
+
# def self.included(base)
|
99
|
+
# base.send :helper_method, :current_user, :logged_in?
|
100
|
+
# end
|
101
|
+
|
102
|
+
# Called from #current_user. First attempt to login by the user id stored in the session.
|
103
|
+
def login_from_session
|
104
|
+
self.current_user = find_user_by_id(session[:user]) if session[:user]
|
105
|
+
end
|
106
|
+
|
107
|
+
# Called from #current_user. Now, attempt to login by basic authentication information.
|
108
|
+
def login_from_basic_auth
|
109
|
+
username, passwd = get_auth_data
|
110
|
+
self.current_user = verify_login(username, passwd) if username && passwd
|
111
|
+
end
|
112
|
+
|
113
|
+
# Called from #current_user. Finaly, attempt to login by an expiring token in the cookie.
|
114
|
+
def login_from_cookie
|
115
|
+
user = cookies[:auth_token] && find_user_by_remember_token(cookies[:auth_token])
|
116
|
+
if user && user.remember_token?
|
117
|
+
user.remember_me
|
118
|
+
cookies[:auth_token] = { :value => user.remember_token, :expires => Time.parse(user.remember_token_expires_at.strftime) }
|
119
|
+
self.current_user = user
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def reset_session
|
124
|
+
session.data.each{|k,v| session.data.delete(k)}
|
125
|
+
end
|
126
|
+
|
127
|
+
protected
|
128
|
+
def verify_login(username, password)
|
129
|
+
AuthSlice::User.authenticate(username, password)
|
130
|
+
end
|
131
|
+
|
132
|
+
def find_user_by_id(user_id)
|
133
|
+
AuthSlice::User.find_by_id(user_id)
|
134
|
+
end
|
135
|
+
|
136
|
+
def find_user_by_remember_token(token)
|
137
|
+
AuthSlice::User.find_by_remember_token(token)
|
138
|
+
end
|
139
|
+
|
140
|
+
private
|
141
|
+
@@http_auth_headers = %w(Authorization HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION)
|
142
|
+
|
143
|
+
# gets BASIC auth info
|
144
|
+
def get_auth_data
|
145
|
+
auth_key = @@http_auth_headers.detect { |h| request.env.has_key?(h) }
|
146
|
+
auth_data = request.env[auth_key].to_s.split unless auth_key.blank?
|
147
|
+
return auth_data && auth_data[0] == 'Basic' ? Base64.decode64(auth_data[1]).split(':')[0..1] : [nil, nil]
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class AuthSlice::Users < AuthSlice::Application
|
2
|
+
provides :html
|
3
|
+
|
4
|
+
skip_before :login_required
|
5
|
+
|
6
|
+
def login
|
7
|
+
if request.post?
|
8
|
+
self.current_user = verify_login(params[:username], params[:password])
|
9
|
+
if logged_in?
|
10
|
+
if params[:remember_me] == "1"
|
11
|
+
self.current_user.remember_me
|
12
|
+
cookies[:auth_token] = {
|
13
|
+
:value => self.current_user.remember_token,
|
14
|
+
:expires => Time.parse(current_user.remember_token_expires_at.strftime)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
return redirect_back_or_default('/')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
render
|
22
|
+
end
|
23
|
+
|
24
|
+
def logout
|
25
|
+
self.current_user.forget_me if logged_in?
|
26
|
+
cookies.delete :auth_token
|
27
|
+
reset_session
|
28
|
+
redirect_back_or_default('/')
|
29
|
+
end
|
30
|
+
|
31
|
+
def signup
|
32
|
+
cookies.delete :auth_token
|
33
|
+
@user = AuthSlice::User.new(params['auth_slice::user'] || {})
|
34
|
+
|
35
|
+
if request.post? && @user.save
|
36
|
+
return redirect_back_or_default('/')
|
37
|
+
end
|
38
|
+
|
39
|
+
render
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Merb
|
2
|
+
module AuthSlice
|
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
|
+
File.join(::AuthSlice.public_dir_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
|
+
File.join(::AuthSlice.app_dir_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
|
+
File.join(::AuthSlice.dir_for(type), *segments)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module AuthSlice
|
2
|
+
module Adapter
|
3
|
+
module Activerecord
|
4
|
+
def self.create_user_model
|
5
|
+
Object.class_eval <<-end_eval
|
6
|
+
class ::AuthSlice::User < ActiveRecord::Base
|
7
|
+
include AuthSlice::Adapter::Activerecord
|
8
|
+
end
|
9
|
+
end_eval
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
include AuthSlice::BaseModel
|
15
|
+
extend ClassMethods
|
16
|
+
|
17
|
+
validates_presence_of :name
|
18
|
+
validates_presence_of :username
|
19
|
+
validates_presence_of :email
|
20
|
+
validates_length_of :username, :within => 3..40
|
21
|
+
validates_length_of :password, :within => 4..40, :if => :password_required?
|
22
|
+
validates_presence_of :password_confirmation, :if => :password_required?
|
23
|
+
validates_confirmation_of :password, :if => :password_required?
|
24
|
+
validates_uniqueness_of :username, :case_sensitive => false, :if => lambda { |u| !u.username.blank? }
|
25
|
+
validates_uniqueness_of :email, :case_sensitive => false, :if => lambda { |u| !u.email.blank? }
|
26
|
+
|
27
|
+
before_save :encrypt_password
|
28
|
+
|
29
|
+
def username=(value)
|
30
|
+
self[:username] = value.downcase if value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def drop_db_table
|
37
|
+
self.connection.drop_table("users")
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_db_table
|
41
|
+
self.connection.create_table("users") do |t|
|
42
|
+
t.string :name, :limit => 40, :null => false
|
43
|
+
t.string :username, :limit => 40, :null => false
|
44
|
+
t.string :email, :null => false
|
45
|
+
t.string :crypted_password, :limit => 40
|
46
|
+
t.string :salt, :limit => 40, :null => false
|
47
|
+
t.string :remember_token
|
48
|
+
t.date :remember_token_expires_at
|
49
|
+
t.timestamps
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|