auth-slice 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|