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
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'dm-validations'
|
2
|
+
require 'dm-timestamps'
|
3
|
+
require 'dm-aggregates'
|
4
|
+
|
5
|
+
module AuthSlice
|
6
|
+
module Adapter
|
7
|
+
module Datamapper
|
8
|
+
def self.create_user_model
|
9
|
+
Object.class_eval <<-end_eval
|
10
|
+
class ::AuthSlice::User
|
11
|
+
include AuthSlice::Adapter::Datamapper
|
12
|
+
end
|
13
|
+
end_eval
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.class_eval do
|
18
|
+
include ::DataMapper::Resource
|
19
|
+
include AuthSlice::BaseModel
|
20
|
+
extend ClassMethods
|
21
|
+
|
22
|
+
storage_names[:default] = 'users'
|
23
|
+
|
24
|
+
property :id, Integer, :serial => true
|
25
|
+
property :name, String, :length => 3..40, :nullable => false
|
26
|
+
property :username, String, :length => 3..40, :nullable => false
|
27
|
+
property :email, String, :format => :email_address, :nullable => false
|
28
|
+
property :crypted_password, String, :length => 40
|
29
|
+
property :salt, String, :length => 40
|
30
|
+
property :remember_token_expires_at, Date
|
31
|
+
property :remember_token, String
|
32
|
+
property :created_at, DateTime
|
33
|
+
property :updated_at, DateTime
|
34
|
+
|
35
|
+
validates_is_unique :username, :email
|
36
|
+
validates_length :password, :in => 4..40, :if => :password_required?
|
37
|
+
validates_is_confirmed :password, :groups => :create
|
38
|
+
|
39
|
+
before :save, :encrypt_password
|
40
|
+
|
41
|
+
def username=(value)
|
42
|
+
attribute_set(:username, value.downcase) if value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def create_db_table
|
49
|
+
self.auto_migrate!
|
50
|
+
end
|
51
|
+
|
52
|
+
def drop_db_table
|
53
|
+
self.repository do |r|
|
54
|
+
r.adapter.destroy_model_storage(r, self)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_by_id(id)
|
59
|
+
AuthSlice::User.first(:id => id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_by_remember_token(rt)
|
63
|
+
AuthSlice::User.first(:remember_token => rt)
|
64
|
+
end
|
65
|
+
|
66
|
+
def find_by_username(username)
|
67
|
+
if AuthSlice::User.properties[:activated_at]
|
68
|
+
AuthSlice::User.first(:username => username, :activated_at.not => nil)
|
69
|
+
else
|
70
|
+
AuthSlice::User.first(:username => username)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def find_by_activiation_code(activation_code)
|
75
|
+
AuthSlice::User.first(:activation_code => activation_code)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
module AuthSlice
|
4
|
+
module BaseModel
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, InstanceMethods)
|
7
|
+
base.send(:extend, ClassMethods)
|
8
|
+
|
9
|
+
base.class_eval do
|
10
|
+
attr_accessor :password, :password_confirmation
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
def authenticated?(password)
|
16
|
+
crypted_password == encrypt(password)
|
17
|
+
end
|
18
|
+
|
19
|
+
# before filter
|
20
|
+
def encrypt_password
|
21
|
+
return if password.blank?
|
22
|
+
self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{username}--") if new_record?
|
23
|
+
self.crypted_password = encrypt(password)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Encrypts the password with the user salt
|
27
|
+
def encrypt(password)
|
28
|
+
self.class.encrypt(password, salt)
|
29
|
+
end
|
30
|
+
|
31
|
+
def remember_token?
|
32
|
+
remember_token_expires_at && Date.today < remember_token_expires_at
|
33
|
+
end
|
34
|
+
|
35
|
+
def remember_me_until(time)
|
36
|
+
self.remember_token_expires_at = time
|
37
|
+
self.remember_token = encrypt("#{email}--#{remember_token_expires_at}")
|
38
|
+
save
|
39
|
+
end
|
40
|
+
|
41
|
+
def remember_me_for(days)
|
42
|
+
remember_me_until (Date.today + days)
|
43
|
+
end
|
44
|
+
|
45
|
+
# These create and unset the fields required for remembering users between browser closes
|
46
|
+
# Default of 2 weeks
|
47
|
+
def remember_me
|
48
|
+
remember_me_for(14)
|
49
|
+
end
|
50
|
+
|
51
|
+
def forget_me
|
52
|
+
self.remember_token_expires_at = nil
|
53
|
+
self.remember_token = nil
|
54
|
+
self.save
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
def password_required?
|
59
|
+
crypted_password.blank? || !password.blank?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module ClassMethods
|
64
|
+
# Encrypts some data with the salt.
|
65
|
+
def encrypt(password, salt)
|
66
|
+
Digest::SHA1.hexdigest("--#{salt}--#{password}--")
|
67
|
+
end
|
68
|
+
|
69
|
+
# Authenticates a user by their username and unencrypted password. Returns the user or nil.
|
70
|
+
def authenticate(username, password)
|
71
|
+
u = find_by_username(username) # need to get the salt
|
72
|
+
u && u.authenticated?(password) ? u : nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,47 @@
|
|
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
|
+
<title>Auth::Slice Sample Layout</title>
|
5
|
+
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
6
|
+
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/reset-fonts-grids/reset-fonts-grids.css">
|
7
|
+
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.1/build/base/base-min.css">
|
8
|
+
<link href="<%= public_path_for :stylesheet, 'master.css' %>" type="text/css" charset="utf-8" rel="stylesheet" media="all"/>
|
9
|
+
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js"></script>
|
10
|
+
<script type="text/javascript">
|
11
|
+
(function($) {
|
12
|
+
$(document).ready(function() {
|
13
|
+
$(":text:visible:enabled:first").focus();
|
14
|
+
});
|
15
|
+
})(jQuery);
|
16
|
+
</script>
|
17
|
+
</head>
|
18
|
+
<body>
|
19
|
+
<div id="container">
|
20
|
+
<div id="header-container">
|
21
|
+
<span style="float:right;margin-right:50px">
|
22
|
+
<% if logged_in? %>
|
23
|
+
Welcome <%= current_user.name %>
|
24
|
+
| <a href="<%= url(:logout) %>">Sign out</a>
|
25
|
+
<% else %>
|
26
|
+
<a href="<%= url(:signup) %>">Join now</a>
|
27
|
+
| <a href="<%= url(:login) %>">Sign in</a>
|
28
|
+
<% end %>
|
29
|
+
</span>
|
30
|
+
<h1>Sample AuthSlice App</h1>
|
31
|
+
<hr />
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div id="main-container">
|
35
|
+
<%= catch_content :for_layout %>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<hr />
|
39
|
+
|
40
|
+
<div id="footer-container">
|
41
|
+
<div class="left"><%= __FILE__ %></div>
|
42
|
+
<div class="right">© 2008 PragmaQuest Inc. All rights reserved.</div>
|
43
|
+
</div>
|
44
|
+
</div>
|
45
|
+
</body>
|
46
|
+
</html>
|
47
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
<div>
|
3
|
+
<div class="form_description">
|
4
|
+
<h2>Member sign in</h2>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<form action="<%= url(:login) %>" method="post">
|
8
|
+
<% if request.post? -%>
|
9
|
+
<div class="error">
|
10
|
+
<h2>Incorrect username and password</h2>
|
11
|
+
</div>
|
12
|
+
<% end -%>
|
13
|
+
|
14
|
+
<p><label for="username">Username or e-mail</label>
|
15
|
+
<input type="text" name="username" value="<%= params[:username] %>"/></p>
|
16
|
+
|
17
|
+
<p><label for="password">Password</label>
|
18
|
+
<input type="password" name="password"/></p>
|
19
|
+
|
20
|
+
<p><input type="checkbox" name='remember_me' id='remember_me'/>
|
21
|
+
<label id='remember_me_lb' for="remember_me">Keep me signed in</label></p>
|
22
|
+
|
23
|
+
<p><input type="submit" value="Sign in"/> <span style="margin-left:20px"><a href="#">Forgot password?</a></span></p>
|
24
|
+
</form>
|
25
|
+
|
26
|
+
<form action="#" method="post" style="display:none">
|
27
|
+
<p><label for="openid_url">OpenID</label>
|
28
|
+
<input type="text" value="" size="40" id="openid_url" name="openid_url"/> <small>(e.g. http://username.myopenid.com)</small>
|
29
|
+
</p>
|
30
|
+
</form>
|
31
|
+
</div>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
<div>
|
3
|
+
<div class="form_description">
|
4
|
+
<h2>Sign up here (it's absolutely free)</h2>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<% form_for @user, :action => url(:signup) do %>
|
8
|
+
<%= error_messages_for :user, lambda{|err| "<li>#{err.join('<br/>')}</li>"} %>
|
9
|
+
|
10
|
+
<p>
|
11
|
+
<%= text_control :name, :label => "Name" %>
|
12
|
+
</p>
|
13
|
+
<p>
|
14
|
+
<%= text_control :username, :label => "Username" %>
|
15
|
+
</p>
|
16
|
+
<p>
|
17
|
+
<%= text_control :email, :label => "E-mail" %>
|
18
|
+
</p>
|
19
|
+
<p>
|
20
|
+
<%= password_control :password, :label => "Password" %>
|
21
|
+
</p>
|
22
|
+
<p>
|
23
|
+
<%= password_control :password_confirmation, :label => "Password Confirmation" %>
|
24
|
+
</p>
|
25
|
+
<p>
|
26
|
+
<%= submit_button "Sign up" %> <span style="margin-left:20px">Already a member? <a href="<%= url(:login) %>">Sign in here</a></span>
|
27
|
+
</p>
|
28
|
+
<% end %>
|
29
|
+
</div>
|
data/lib/auth-slice.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
if defined?(Merb::Plugins)
|
2
|
+
|
3
|
+
require 'merb-slices'
|
4
|
+
require 'merb_helpers'
|
5
|
+
require File.join(File.dirname(__FILE__), 'auth-slice', 'model')
|
6
|
+
|
7
|
+
Merb::Plugins.add_rakefiles "auth-slice/merbtasks"
|
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.
|
14
|
+
Merb::Slices::config[:auth_slice] = { :layout => :auth_slice }
|
15
|
+
|
16
|
+
# All Slice code is expected to be namespaced inside a module
|
17
|
+
module AuthSlice
|
18
|
+
# Slice metadata
|
19
|
+
self.description = "AuthSlice is an user authentication Merb slice!"
|
20
|
+
self.version = "0.1.0"
|
21
|
+
self.author = "ctran@pragmaquest.com"
|
22
|
+
|
23
|
+
# Initialization hook - runs before AfterAppLoads BootLoader
|
24
|
+
def self.init
|
25
|
+
end
|
26
|
+
|
27
|
+
# Activation hook - runs after AfterAppLoads BootLoader
|
28
|
+
def self.activate
|
29
|
+
AuthSlice.use_adapter(Merb.orm_generator_scope) if Merb.orm_generator_scope != :merb_default
|
30
|
+
Object.const_set(Merb::Slices::config[:auth_slice][:user_model_class], AuthSlice::User) if Merb::Slices::config[:auth_slice][:user_model_class]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Deactivation hook - triggered by Merb::Slices#deactivate
|
34
|
+
def self.deactivate
|
35
|
+
end
|
36
|
+
|
37
|
+
# Setup routes inside the host application
|
38
|
+
#
|
39
|
+
# @param scope<Merb::Router::Behaviour>
|
40
|
+
# Routes will be added within this scope (namespace). In fact, any
|
41
|
+
# router behaviour is a valid namespace, so you can attach
|
42
|
+
# routes at any level of your router setup.
|
43
|
+
def self.setup_router(scope)
|
44
|
+
scope.match('/login').to(:controller => 'users', :action => 'login').name(:login)
|
45
|
+
scope.match('/logout').to(:controller => 'users', :action => 'logout').name(:logout)
|
46
|
+
scope.match('/signup').to(:controller => 'users', :action => 'signup').name(:signup)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Setup the slice layout for AuthSlice
|
51
|
+
#
|
52
|
+
# Use AuthSlice.push_path and AuthSlice.push_app_path
|
53
|
+
# to set paths to auth-slice-level and app-level paths. Example:
|
54
|
+
#
|
55
|
+
# AuthSlice.push_path(:application, AuthSlice.root)
|
56
|
+
# AuthSlice.push_app_path(:application, Merb.root / 'slices' / 'auth-slice')
|
57
|
+
# ...
|
58
|
+
#
|
59
|
+
# Any component path that hasn't been set will default to AuthSlice.root
|
60
|
+
#
|
61
|
+
# Or just call setup_default_structure! to setup a basic Merb MVC structure.
|
62
|
+
AuthSlice.setup_default_structure!
|
63
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
$SLICED_APP=true # we're running inside the host application context
|
2
|
+
|
3
|
+
namespace :slices do
|
4
|
+
namespace :auth_slice do
|
5
|
+
|
6
|
+
desc "Install AuthSlice"
|
7
|
+
task :install => [:preflight, :setup_directories, :copy_assets, :migrate]
|
8
|
+
|
9
|
+
desc "Test for any dependencies"
|
10
|
+
task :preflight do
|
11
|
+
# implement this to test for structural/code dependencies
|
12
|
+
# like certain directories or availability of other files
|
13
|
+
end
|
14
|
+
|
15
|
+
desc "Setup directories"
|
16
|
+
task :setup_directories do
|
17
|
+
puts "Creating directories for host application"
|
18
|
+
[:application, :view, :model, :controller, :helper, :mailer, :part, :public].each do |type|
|
19
|
+
if File.directory?(AuthSlice.dir_for(type))
|
20
|
+
if !File.directory?(dst_path = AuthSlice.app_dir_for(type))
|
21
|
+
relative_path = dst_path.relative_path_from(Merb.root)
|
22
|
+
puts "- creating directory :#{type} #{File.basename(Merb.root) / relative_path}"
|
23
|
+
mkdir_p(dst_path)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Copy public assets to host application"
|
30
|
+
task :copy_assets do
|
31
|
+
puts "Copying assets for AuthSlice - do not edit these as the will be overwritten!"
|
32
|
+
[:image, :javascript, :stylesheet].each do |type|
|
33
|
+
src_path = AuthSlice.dir_for(type)
|
34
|
+
dst_path = AuthSlice.app_dir_for(type)
|
35
|
+
Dir[src_path / '**/*'].each do |file|
|
36
|
+
relative_path = file.relative_path_from(src_path)
|
37
|
+
puts "- installing :#{type} #{relative_path}"
|
38
|
+
mkdir_p(dst_path / File.dirname(relative_path))
|
39
|
+
copy_entry(file, dst_path / relative_path, false, false, true)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "Migrate the database"
|
45
|
+
task :migrate do
|
46
|
+
AuthSlice::User.create_db_table
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "Run slice specs within the host application context"
|
50
|
+
task :spec => [ "spec:explain", "spec:default" ]
|
51
|
+
|
52
|
+
namespace :spec do
|
53
|
+
|
54
|
+
slice_root = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
55
|
+
|
56
|
+
task :explain do
|
57
|
+
puts "\nNote: By running AuthSlice specs inside the application context any\n" +
|
58
|
+
"overrides could break existing specs. This isn't always a problem,\n" +
|
59
|
+
"especially in the case of views. Use these spec tasks to check how\n" +
|
60
|
+
"well your application conforms to the original slice implementation."
|
61
|
+
end
|
62
|
+
|
63
|
+
Spec::Rake::SpecTask.new('default') do |t|
|
64
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
65
|
+
t.spec_files = Dir["#{slice_root}/spec/**/*_spec.rb"].sort
|
66
|
+
end
|
67
|
+
|
68
|
+
desc "Run all model specs, run a spec for a specific Model with MODEL=MyModel"
|
69
|
+
Spec::Rake::SpecTask.new('model') do |t|
|
70
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
71
|
+
if(ENV['MODEL'])
|
72
|
+
t.spec_files = Dir["#{slice_root}/spec/models/**/#{ENV['MODEL']}_spec.rb"].sort
|
73
|
+
else
|
74
|
+
t.spec_files = Dir["#{slice_root}/spec/models/**/*_spec.rb"].sort
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
desc "Run all controller specs, run a spec for a specific Controller with CONTROLLER=MyController"
|
79
|
+
Spec::Rake::SpecTask.new('controller') do |t|
|
80
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
81
|
+
if(ENV['CONTROLLER'])
|
82
|
+
t.spec_files = Dir["#{slice_root}/spec/controllers/**/#{ENV['CONTROLLER']}_spec.rb"].sort
|
83
|
+
else
|
84
|
+
t.spec_files = Dir["#{slice_root}/spec/controllers/**/*_spec.rb"].sort
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "Run all view specs, run specs for a specific controller (and view) with CONTROLLER=MyController (VIEW=MyView)"
|
89
|
+
Spec::Rake::SpecTask.new('view') do |t|
|
90
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
91
|
+
if(ENV['CONTROLLER'] and ENV['VIEW'])
|
92
|
+
t.spec_files = Dir["#{slice_root}/spec/views/**/#{ENV['CONTROLLER']}/#{ENV['VIEW']}*_spec.rb"].sort
|
93
|
+
elsif(ENV['CONTROLLER'])
|
94
|
+
t.spec_files = Dir["#{slice_root}/spec/views/**/#{ENV['CONTROLLER']}/*_spec.rb"].sort
|
95
|
+
else
|
96
|
+
t.spec_files = Dir["#{slice_root}/spec/views/**/*_spec.rb"].sort
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
desc "Run all specs and output the result in html"
|
101
|
+
Spec::Rake::SpecTask.new('html') do |t|
|
102
|
+
t.spec_opts = ["--format", "html"]
|
103
|
+
t.libs = ['lib', 'server/lib' ]
|
104
|
+
t.spec_files = Dir["#{slice_root}/spec/**/*_spec.rb"].sort
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|