sinatra-authentication 0.0.5
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/.gitignore +4 -0
- data/History.txt +4 -0
- data/Manifest +24 -0
- data/Rakefile +25 -0
- data/TODO +53 -0
- data/lib/models/abstract_user.rb +49 -0
- data/lib/models/datamapper_user.rb +38 -0
- data/lib/models/dm_adapter.rb +44 -0
- data/lib/models/rufus_tokyo_user.rb +146 -0
- data/lib/models/tc_adapter.rb +73 -0
- data/lib/sinatra-authentication.rb +206 -0
- data/lib/views/edit.haml +21 -0
- data/lib/views/index.haml +23 -0
- data/lib/views/login.haml +10 -0
- data/lib/views/show.haml +6 -0
- data/lib/views/signup.haml +13 -0
- data/readme.markdown +92 -0
- data/test/datamapper_test.rb +5 -0
- data/test/lib/dm_app.rb +18 -0
- data/test/lib/helper.rb +9 -0
- data/test/lib/tc_app.rb +16 -0
- data/test/route_tests.rb +29 -0
- data/test/rufus_tokyo_test.rb +5 -0
- metadata +131 -0
data/.gitignore
ADDED
data/History.txt
ADDED
data/Manifest
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest
|
3
|
+
Rakefile
|
4
|
+
TODO
|
5
|
+
lib/models/abstract_user.rb
|
6
|
+
lib/models/datamapper_user.rb
|
7
|
+
lib/models/dm_adapter.rb
|
8
|
+
lib/models/rufus_tokyo_user.rb
|
9
|
+
lib/models/tc_adapter.rb
|
10
|
+
lib/sinatra-authentication.rb
|
11
|
+
lib/views/edit.haml
|
12
|
+
lib/views/index.haml
|
13
|
+
lib/views/login.haml
|
14
|
+
lib/views/show.haml
|
15
|
+
lib/views/signup.haml
|
16
|
+
readme.markdown
|
17
|
+
test/datamapper_test.rb
|
18
|
+
test/lib/dm_app.rb
|
19
|
+
test/lib/helper.rb
|
20
|
+
test/lib/tc_app.rb
|
21
|
+
test/lib/test.db
|
22
|
+
test/lib/users.tct
|
23
|
+
test/route_tests.rb
|
24
|
+
test/rufus_tokyo_test.rb
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
|
7
|
+
Jeweler::Tasks.new do |gemspec|
|
8
|
+
gemspec.name = 'sinatra-authentication'
|
9
|
+
gemspec.version = '0.0.5'
|
10
|
+
gemspec.description = "Simple authentication plugin for sinatra."
|
11
|
+
gemspec.summary = "Simple authentication plugin for sinatra."
|
12
|
+
gemspec.homepage = "http://github.com/maxjustus/sinatra-authentication"
|
13
|
+
gemspec.author = "Max Justus Spransy"
|
14
|
+
gemspec.email = "maxjustus@gmail.com"
|
15
|
+
gemspec.add_dependency "sinatra"
|
16
|
+
gemspec.add_dependency "dm-core"
|
17
|
+
gemspec.add_dependency "dm-validations"
|
18
|
+
gemspec.add_dependency "dm-timestamps"
|
19
|
+
gemspec.add_dependency "rufus-tokyo"
|
20
|
+
end
|
21
|
+
rescue LoadError
|
22
|
+
puts "Jeweler (or a dependency) not available. Install it first!"
|
23
|
+
end
|
24
|
+
|
25
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/TODO
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
TODO:
|
2
|
+
ensure all calls to new in adaptors are equivelant to "find or create by"
|
3
|
+
- this is done in the Tc adaptor, and essentially works in Dm adaptor
|
4
|
+
because I validate the uniqueness of email
|
5
|
+
implement some way to allow for the creation of users with different
|
6
|
+
permission levels in an untamperable manner. Perhaps with some secret key
|
7
|
+
that must be sent in the form
|
8
|
+
- look at other permissions systems for some feature ideas
|
9
|
+
- add a config method that you pass a hash for configuring it's behavior
|
10
|
+
- secret signup urls
|
11
|
+
- account activation through email
|
12
|
+
|
13
|
+
- ?implement a session store which isn't cookie based
|
14
|
+
- turn on sessions unless they're already on
|
15
|
+
- randomize the session key on every installation?
|
16
|
+
- clean up adapters
|
17
|
+
- write simple attribute declaration method for TcUser
|
18
|
+
- condense the adapters down to the simplest solution that could possibly work
|
19
|
+
- right now it's like I have two seperate goals, both which are important
|
20
|
+
one is to write a simple ORM for rufus tokyo, the other is to create a simple adapter
|
21
|
+
for different database backends. I think it would be better if I made the datamapper adapter more abstract
|
22
|
+
and the api simpler, and then changed the way the controllers and views work to interface with the more abstract and simpler adapter.
|
23
|
+
maybe make the adapter class called UserAdapter, and then TkUser and DmUser become User. All my controller method calls go to UserAdapter
|
24
|
+
but then for people wanting to talk to the model, they just use User, since they aren't dealing with multiple backends and thus don't need
|
25
|
+
a creepy adapter.
|
26
|
+
|
27
|
+
- make site admin work the same for dm and tc, because I like how permission == -2 is site admin, then you could set a user as site admin instead of it being limited to the first user created
|
28
|
+
and they wouldn't be deletable.
|
29
|
+
or maybe I just make a heirarchy for that so users with lower permissions can't delete users with higher.
|
30
|
+
just remember, this is supposed to be a simple authentication solution
|
31
|
+
|
32
|
+
|
33
|
+
- for the User adapter
|
34
|
+
- add pagination to all
|
35
|
+
- serious cleanup of rufus_tokyo_user.rb
|
36
|
+
- add virtual attribute declaration
|
37
|
+
- add validations
|
38
|
+
- remove the object syntax method_missing? and stick to hash accessors?
|
39
|
+
- or rather then use method missing, dynamically create class methods based in the contents of the hash?
|
40
|
+
- or create a validator tool for hashes. hash.valid?
|
41
|
+
- change User to AbstractUser and DmUser and TcUser to User
|
42
|
+
|
43
|
+
- add error messages for failed logins and stuff
|
44
|
+
|
45
|
+
- PROBLEM the way I have method missing working right now, it doesn't behave the same way I've documented, since I use it for attributes
|
46
|
+
- throw configuration errors on startup
|
47
|
+
- investigate why sinatra_auth doesn't seem to work unless it's the last thing required..
|
48
|
+
|
49
|
+
- add facebook connect
|
50
|
+
- using facebooker and frankie
|
51
|
+
- using the same method as datamapper vs tokyo in that the functionality is only included if the libraries are required
|
52
|
+
before sinatra_auth is.
|
53
|
+
- when a user signs in using facebook and doesn't have an email specified, an email field is included in the edit form.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
if Object.const_defined?("DataMapper")
|
2
|
+
#require 'dm-core'
|
3
|
+
require 'dm-timestamps'
|
4
|
+
require 'dm-validations'
|
5
|
+
require Pathname(__FILE__).dirname.expand_path + "datamapper_user.rb"
|
6
|
+
require Pathname(__FILE__).dirname.expand_path + "dm_adapter.rb"
|
7
|
+
elsif Object.const_defined?("Rufus")
|
8
|
+
require Pathname(__FILE__).dirname.expand_path + "rufus_tokyo_user.rb"
|
9
|
+
require Pathname(__FILE__).dirname.expand_path + "tc_adapter.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
class User
|
13
|
+
if Object.const_defined?("DataMapper")
|
14
|
+
include DmAdapter
|
15
|
+
elsif Object.const_defined?("Rufus")
|
16
|
+
include TcAdapter
|
17
|
+
else
|
18
|
+
throw "you need to require either 'dm-core' or 'rufus-tokyo' for sinatra-authentication to work"
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(interfacing_class_instance)
|
22
|
+
@instance = interfacing_class_instance
|
23
|
+
end
|
24
|
+
|
25
|
+
def id
|
26
|
+
@instance.id
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.authenticate(email, pass)
|
30
|
+
current_user = get(:email => email)
|
31
|
+
return nil if current_user.nil?
|
32
|
+
return current_user if User.encrypt(pass, current_user.salt) == current_user.hashed_password
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def self.encrypt(pass, salt)
|
39
|
+
Digest::SHA1.hexdigest(pass+salt)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.random_string(len)
|
43
|
+
#generate a random password consisting of strings and digits
|
44
|
+
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
45
|
+
newpass = ""
|
46
|
+
1.upto(len) { |i| newpass << chars[rand(chars.size-1)] }
|
47
|
+
return newpass
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class DmUser
|
2
|
+
include DataMapper::Resource
|
3
|
+
|
4
|
+
property :id, Serial
|
5
|
+
property :email, String, :key => true, :nullable => false, :length => (5..40), :unique => true, :format => :email_address
|
6
|
+
property :hashed_password, String
|
7
|
+
property :salt, String, :nullable => false
|
8
|
+
property :created_at, DateTime
|
9
|
+
property :permission_level, Integer, :default => 1
|
10
|
+
|
11
|
+
attr_accessor :password, :password_confirmation
|
12
|
+
#protected equievelant? :protected => true doesn't exist in dm 0.10.0
|
13
|
+
#protected :id, :salt
|
14
|
+
#doesn't behave correctly, I'm not even sure why I did this.
|
15
|
+
|
16
|
+
validates_present :password_confirmation, :unless => Proc.new { |t| t.hashed_password }
|
17
|
+
validates_present :password, :unless => Proc.new { |t| t.hashed_password }
|
18
|
+
validates_is_confirmed :password
|
19
|
+
|
20
|
+
def password=(pass)
|
21
|
+
@password = pass
|
22
|
+
self.salt = User.random_string(10) if !self.salt
|
23
|
+
self.hashed_password = User.encrypt(@password, self.salt)
|
24
|
+
end
|
25
|
+
|
26
|
+
def admin?
|
27
|
+
self.permission_level == -1 || self.id == 1
|
28
|
+
end
|
29
|
+
|
30
|
+
def site_admin?
|
31
|
+
self.id == 1
|
32
|
+
end
|
33
|
+
protected
|
34
|
+
|
35
|
+
def method_missing(m, *args)
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module DmAdapter
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
base.class_eval { include DmAdapter::InstanceMethods }
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def all
|
9
|
+
result = DmUser.all
|
10
|
+
result.collect {|instance| self.new instance}
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(hash)
|
14
|
+
if user = DmUser.first(hash)
|
15
|
+
self.new user
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(attributes)
|
22
|
+
user = DmUser.new attributes
|
23
|
+
user.save
|
24
|
+
user
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(pk)
|
28
|
+
user = User.first(:id => pk)
|
29
|
+
user.destroy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceMethods
|
34
|
+
def update(attributes)
|
35
|
+
@instance.update_attributes attributes
|
36
|
+
@instance.save
|
37
|
+
end
|
38
|
+
|
39
|
+
def method_missing(meth, *args, &block)
|
40
|
+
#cool I just found out * on an array turns the array into a list of args for a function
|
41
|
+
@instance.send(meth, *args, &block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
class TcUser
|
2
|
+
#include RufusOrm
|
3
|
+
|
4
|
+
#custom_attribute :salt
|
5
|
+
#custom_attribute :hashed_password
|
6
|
+
#custom_attribute :hashed_permission_level
|
7
|
+
#custom_attribute :created_at
|
8
|
+
#custom_attribute :created_at_i
|
9
|
+
|
10
|
+
#attribute method?
|
11
|
+
#if I'm gonna write all this, I might as well create a tinyyyy
|
12
|
+
#orm, that's more just like a way to define custom attributes for cabinets
|
13
|
+
#something worth noting though is that even datamapper defines custom
|
14
|
+
#attributes by allowing the developer to override setter methods.
|
15
|
+
#and it just calls all the setter methods defined in the model.
|
16
|
+
#the only trouble with this route is it assumes a predefined schema.
|
17
|
+
#and thus it knows what setter methods to call.
|
18
|
+
#I would write a class method that allows you to declare attributes like
|
19
|
+
#attribute :salt, with an optional block (which gets passed a hash of attributes)
|
20
|
+
#if a block isn't defined, it looks in the class for a salt=(attributes) function, calls it and marges the
|
21
|
+
#result into the hash going into the database, like 'attributes.merge{"salt" => result}'
|
22
|
+
#so my 'set' method or whatever I choose to call it, has to somehow look through each
|
23
|
+
#declared attribute, call the method associated with it, and merge the result into the hash going
|
24
|
+
#into the database.
|
25
|
+
#
|
26
|
+
#but what if I don't want an attribute passed in to be stored into the database? What if I just want to
|
27
|
+
#create a virtual attribute, for declaring other attributes?
|
28
|
+
#I might create a class variable that I store all the attributes in, and the I can get to it from any setter,
|
29
|
+
#and then after I've called all the setters, I store that class variable into the database.
|
30
|
+
#or, I do all of this on the instance level, and have a save method.
|
31
|
+
|
32
|
+
def initialize(attributes)
|
33
|
+
@attributes = attributes
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.query(&block)
|
37
|
+
connection = TcUserTable.new
|
38
|
+
result_set = connection.query(&block)
|
39
|
+
result_set.collect! { |result_hash| TcUser.new(result_hash) }
|
40
|
+
connection.close
|
41
|
+
result_set
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.get(key)
|
45
|
+
connection = TcUserTable.new
|
46
|
+
result = connection[key]
|
47
|
+
connection.close
|
48
|
+
if result
|
49
|
+
self.new(result.merge({:pk => key}))
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.set(attributes)
|
56
|
+
#this way of validating is real crap, replace it with Validator maybe
|
57
|
+
#and maybe replace all this hash merging with setters for the various attributes that update @attributes, and then I can call save to store to the database
|
58
|
+
#or maybe just write a little method that makes hash merger look a little cleaner
|
59
|
+
pk = attributes.delete(:pk) if attributes[:pk]
|
60
|
+
|
61
|
+
email_regexp = /(\A(\s*)\Z)|(\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z)/i
|
62
|
+
if (attributes['password'] == attributes.delete('password_confirmation') && attributes['email'] =~ email_regexp)
|
63
|
+
password = attributes.delete('password')
|
64
|
+
attributes.merge!({'salt' => User.random_string(10)}) if !attributes['salt']
|
65
|
+
attributes.merge!('hashed_password' => User.encrypt(password, attributes['salt']))
|
66
|
+
permission_level = attributes['permission_level'] ? attributes['permission_level'] : '1'
|
67
|
+
attributes.merge!('permission_level' => permission_level)
|
68
|
+
attributes.merge!('created_at' => Time.now.to_s)
|
69
|
+
attributes.merge!('created_at_i' => Time.now.to_i.to_s)
|
70
|
+
|
71
|
+
connection = TcUserTable.new
|
72
|
+
pk ||= connection.genuid.to_s
|
73
|
+
#site admin if their first
|
74
|
+
attributes.merge!({'permission_level' => '-2'}) if pk == '1'
|
75
|
+
result = connection[pk] = attributes
|
76
|
+
#might not need this in newer version of rufus
|
77
|
+
result.merge!({:pk => pk})
|
78
|
+
connection.close
|
79
|
+
self.new(result)
|
80
|
+
else
|
81
|
+
false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.delete(pk)
|
86
|
+
connection = TcUserTable.new
|
87
|
+
connection.delete(pk)
|
88
|
+
connection.close
|
89
|
+
end
|
90
|
+
|
91
|
+
def update(attributes)
|
92
|
+
new_attributes = @attributes.merge(attributes)
|
93
|
+
TcUser.set(new_attributes)
|
94
|
+
end
|
95
|
+
|
96
|
+
def [](key)
|
97
|
+
@attributes[key]
|
98
|
+
end
|
99
|
+
|
100
|
+
#saves to database and returns self
|
101
|
+
def []=(key, value)
|
102
|
+
@attributes[key] = value
|
103
|
+
#change so that it sets the attributes and then you call save to save to the database?
|
104
|
+
connection = TcUserTable.new
|
105
|
+
connection[@attributes[:pk]] = @attributes.merge!({key => value})
|
106
|
+
connection.close
|
107
|
+
self
|
108
|
+
end
|
109
|
+
|
110
|
+
def id
|
111
|
+
@attributes[:pk]
|
112
|
+
end
|
113
|
+
|
114
|
+
def admin?
|
115
|
+
#-2 is the site admin
|
116
|
+
@attributes['permission_level'] == '-1' || @attributes['permission_level'] == '-2'
|
117
|
+
end
|
118
|
+
|
119
|
+
def site_admin?
|
120
|
+
@attributes['permission_level'] == '-2'
|
121
|
+
end
|
122
|
+
|
123
|
+
#from hash extension for making hashes like javascript objects
|
124
|
+
def method_missing(meth,*args)
|
125
|
+
if /=$/=~(meth=meth.id2name) then
|
126
|
+
self[meth[0...-1]] = (args.length<2 ? args[0] : args)
|
127
|
+
elsif @attributes[meth]
|
128
|
+
@attributes[meth]
|
129
|
+
else
|
130
|
+
false
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class TcUserTable < Rufus::Tokyo::Table
|
136
|
+
@@path = false
|
137
|
+
def initialize
|
138
|
+
#make this path configurable somehow
|
139
|
+
raise "you need to define a path for the user cabinet to be stored at, like so: TcUserTable.cabinet_path = 'folder/where/you/wanna/store/your/database'" unless @@path
|
140
|
+
super(@@path + '/users.tct')
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.cabinet_path=(path)
|
144
|
+
@@path = path
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module TcAdapter
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
base.class_eval {
|
5
|
+
include TcAdapter::InstanceMethods
|
6
|
+
alias :class_id :id
|
7
|
+
}
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
#TODO add pagination
|
12
|
+
def all
|
13
|
+
result = TcUser.query do |q|
|
14
|
+
q.order_by 'created_at_i', :numdesc
|
15
|
+
end
|
16
|
+
|
17
|
+
#these will be the same for all adapters, they should be defined in the user class, and these methods should have a different name?
|
18
|
+
result.collect {|instance| self.new instance }
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(hash)
|
22
|
+
#because with TcUser email and id are the same because the email is the id
|
23
|
+
if hash[:email]
|
24
|
+
result = TcUser.query do |q|
|
25
|
+
q.add 'email', :streq, hash[:email]
|
26
|
+
end[0]
|
27
|
+
#the zero is because this returns an array but get should return the first result
|
28
|
+
elsif hash[:id]
|
29
|
+
pk = hash[:id]
|
30
|
+
result = TcUser.get(pk)
|
31
|
+
end
|
32
|
+
|
33
|
+
if result
|
34
|
+
self.new result
|
35
|
+
else
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set(attributes)
|
41
|
+
user = TcUser.query do |q|
|
42
|
+
q.add 'email', :streq, attributes['email']
|
43
|
+
end
|
44
|
+
|
45
|
+
if user == [] #no user
|
46
|
+
self.new TcUser.set(attributes)
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(pk)
|
53
|
+
#true or false
|
54
|
+
!!TcUser.delete(pk)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
module InstanceMethods
|
59
|
+
def update(attributes)
|
60
|
+
@instance.update attributes
|
61
|
+
end
|
62
|
+
|
63
|
+
def method_missing(meth, *args, &block)
|
64
|
+
#cool I just found out * on an array turn the array into a list of args for a function
|
65
|
+
@instance.send(meth, *args, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
#this was the only thing that didn't get passed on to method_missing because this is a method of object doh
|
69
|
+
def id
|
70
|
+
@instance.id
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'pathname'
|
3
|
+
require Pathname(__FILE__).dirname.expand_path + "models/abstract_user"
|
4
|
+
|
5
|
+
module SinatraAuthentication
|
6
|
+
VERSION = "0.0.3"
|
7
|
+
end
|
8
|
+
|
9
|
+
module Sinatra
|
10
|
+
module LilAuthentication
|
11
|
+
def self.registered(app)
|
12
|
+
#INVESTIGATE
|
13
|
+
#the possibility of sinatra having an array of view_paths to load from
|
14
|
+
#PROBLEM
|
15
|
+
#sinatra 9.1.1 doesn't have multiple view capability anywhere
|
16
|
+
#so to get around I have to do it totally manually by
|
17
|
+
#loading the view from this path into a string and rendering it
|
18
|
+
set :lil_authentication_view_path, Pathname(__FILE__).dirname.expand_path + "views/"
|
19
|
+
|
20
|
+
#TODO write captain sinatra developer man and inform him that the documentation
|
21
|
+
#concerning the writing of extensions is somewhat outdaded/incorrect.
|
22
|
+
#you do not need to to do self.get/self.post when writing an extension
|
23
|
+
#In fact, it doesn't work. You have to use the plain old sinatra DSL
|
24
|
+
|
25
|
+
get '/users' do
|
26
|
+
@users = User.all
|
27
|
+
if @users != []
|
28
|
+
haml get_view_as_string("index.haml"), :layout => use_layout?
|
29
|
+
else
|
30
|
+
redirect '/signup'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
get '/users/:id' do
|
35
|
+
login_required
|
36
|
+
|
37
|
+
#INVESTIGATE
|
38
|
+
#
|
39
|
+
#WHY THE HECK WON'T GET RETURN ANYTHING?
|
40
|
+
#if I user User.get(params[:id]) it returns nil for some inexplicable reason
|
41
|
+
@user = User.get(:id => params[:id])
|
42
|
+
haml get_view_as_string("show.haml"), :layout => use_layout?
|
43
|
+
end
|
44
|
+
|
45
|
+
#convenience for ajax but maybe entirely stupid and unnecesary
|
46
|
+
get '/logged_in' do
|
47
|
+
if session[:user]
|
48
|
+
"true"
|
49
|
+
else
|
50
|
+
"false"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
get '/login' do
|
55
|
+
haml get_view_as_string("login.haml"), :layout => use_layout?
|
56
|
+
end
|
57
|
+
|
58
|
+
post '/login' do
|
59
|
+
if user = User.authenticate(params[:email], params[:password])
|
60
|
+
session[:user] = user.id
|
61
|
+
redirect '/'
|
62
|
+
else
|
63
|
+
redirect '/login'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
get '/logout' do
|
68
|
+
session[:user] = nil
|
69
|
+
@message = "in case it weren't obvious, you've logged out"
|
70
|
+
redirect '/'
|
71
|
+
end
|
72
|
+
|
73
|
+
get '/signup' do
|
74
|
+
haml get_view_as_string("signup.haml"), :layout => use_layout?
|
75
|
+
end
|
76
|
+
|
77
|
+
post '/signup' do
|
78
|
+
@user = User.set(params[:user])
|
79
|
+
if @user
|
80
|
+
session[:user] = @user.id
|
81
|
+
redirect '/'
|
82
|
+
else
|
83
|
+
session[:flash] = "failure!"
|
84
|
+
redirect '/'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
get '/users/:id/edit' do
|
89
|
+
login_required
|
90
|
+
redirect "/users" unless current_user.admin? || current_user == params[:id]
|
91
|
+
|
92
|
+
@user = User.get(:id => params[:id])
|
93
|
+
haml get_view_as_string("edit.haml"), :layout => use_layout?
|
94
|
+
end
|
95
|
+
|
96
|
+
post '/users/:id/edit' do
|
97
|
+
login_required
|
98
|
+
redirect "/users" unless current_user.admin? || current_user == params[:id]
|
99
|
+
|
100
|
+
user = User.get(:id => params[:id])
|
101
|
+
user_attributes = params[:user]
|
102
|
+
if params[:user][:password] == ""
|
103
|
+
user_attributes.delete("password")
|
104
|
+
user_attributes.delete("password_confirmation")
|
105
|
+
end
|
106
|
+
|
107
|
+
if user.update(user_attributes)
|
108
|
+
redirect "/users/#{user.id}"
|
109
|
+
else
|
110
|
+
throw user.errors
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
get '/users/:id/delete' do
|
115
|
+
login_required
|
116
|
+
redirect "/users" unless current_user.admin? || current_user == params[:id]
|
117
|
+
|
118
|
+
if User.delete(params[:id])
|
119
|
+
session[:flash] = "way to go, you deleted a user"
|
120
|
+
else
|
121
|
+
session[:flash] = "deletion failed, for whatever reason"
|
122
|
+
end
|
123
|
+
redirect '/'
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
module Helpers
|
129
|
+
def login_required
|
130
|
+
if session[:user]
|
131
|
+
return true
|
132
|
+
else
|
133
|
+
session[:return_to] = request.fullpath
|
134
|
+
redirect '/login'
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def current_user
|
140
|
+
if session[:user]
|
141
|
+
User.get(:id => session[:user])
|
142
|
+
else
|
143
|
+
GuestUser.new
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def logged_in?
|
148
|
+
!!session[:user]
|
149
|
+
end
|
150
|
+
|
151
|
+
def use_layout?
|
152
|
+
!request.xhr?
|
153
|
+
end
|
154
|
+
|
155
|
+
#BECAUSE sinatra 9.1.1 can't load views from different paths properly
|
156
|
+
def get_view_as_string(filename)
|
157
|
+
view = options.lil_authentication_view_path + filename
|
158
|
+
data = ""
|
159
|
+
f = File.open(view, "r")
|
160
|
+
f.each_line do |line|
|
161
|
+
data += line
|
162
|
+
end
|
163
|
+
return data
|
164
|
+
end
|
165
|
+
|
166
|
+
def render_login_logout(html_attributes = {:class => ""})
|
167
|
+
css_classes = html_attributes.delete(:class)
|
168
|
+
parameters = ''
|
169
|
+
html_attributes.each_pair do |attribute, value|
|
170
|
+
parameters += "#{attribute}=\"#{value}\" "
|
171
|
+
end
|
172
|
+
|
173
|
+
result = "<div id='sinatra-authentication-login-logout' >"
|
174
|
+
if logged_in?
|
175
|
+
logout_parameters = html_attributes
|
176
|
+
# a tad janky?
|
177
|
+
logout_parameters.delete(:rel)
|
178
|
+
result += "<a href='/users/#{current_user.id}/edit' class='#{css_classes} sinatra-authentication-edit' #{parameters}>edit account</a> "
|
179
|
+
result += "<a href='/logout' class='#{css_classes} sinatra-authentication-logout' #{logout_parameters}>logout</a>"
|
180
|
+
else
|
181
|
+
result += "<a href='/signup' class='#{css_classes} sinatra-authentication-signup' #{parameters}>signup</a> "
|
182
|
+
result += "<a href='/login' class='#{css_classes} sinatra-authentication-login' #{parameters}>login</a>"
|
183
|
+
end
|
184
|
+
|
185
|
+
result += "</div>"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
register LilAuthentication
|
190
|
+
end
|
191
|
+
|
192
|
+
class GuestUser
|
193
|
+
def guest?
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
def permission_level
|
198
|
+
0
|
199
|
+
end
|
200
|
+
|
201
|
+
# current_user.admin? returns false. current_user.has_a_baby? returns false.
|
202
|
+
# (which is a bit of an assumption I suppose)
|
203
|
+
def method_missing(m, *args)
|
204
|
+
return false
|
205
|
+
end
|
206
|
+
end
|
data/lib/views/edit.haml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#sinatra_authentication
|
2
|
+
%h1
|
3
|
+
Editing
|
4
|
+
= @user.email
|
5
|
+
%form{:action => "/users/#{@user.id}/edit", :method => "post"}
|
6
|
+
%input{ :id => "user_password", :name => "user[password]", :size => 30, :type => "password" }
|
7
|
+
new password
|
8
|
+
%br
|
9
|
+
%input{ :id => "user_password_confirmation", :name => "user[password_confirmation]", :size => 30, :type => "password" }
|
10
|
+
confirm
|
11
|
+
-# don't render permission field if admin and editing yourself so you don't shoot yourself in the foot
|
12
|
+
- if current_user.admin? && current_user.id != @user.id
|
13
|
+
%br
|
14
|
+
%select{ :id => "permission_level", :name => "user[permission_level]" }
|
15
|
+
%option{:value => -1, :selected => @user.admin?}
|
16
|
+
admin
|
17
|
+
%option{:value => 1, :selected => @user.permission_level == 1}
|
18
|
+
authenticated user
|
19
|
+
permission level
|
20
|
+
%br
|
21
|
+
%input{ :value => "update", :type => "submit" }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#sinatra_authentication
|
2
|
+
%h1 Users
|
3
|
+
%table
|
4
|
+
%tr
|
5
|
+
%th email
|
6
|
+
- if current_user.admin?
|
7
|
+
%th permission level
|
8
|
+
- @users.each do |user|
|
9
|
+
%tr
|
10
|
+
%td= user.email
|
11
|
+
- if current_user.admin?
|
12
|
+
%td= user.permission_level
|
13
|
+
%td
|
14
|
+
%a{:href => "/users/#{user.id}"} show
|
15
|
+
- if current_user.admin?
|
16
|
+
%td
|
17
|
+
%a{:href => "/users/#{user.id}/edit"} edit
|
18
|
+
%td
|
19
|
+
-# this doesn't work for tk
|
20
|
+
- if !user.site_admin?
|
21
|
+
%a{:href => "/users/#{user.id}/delete", :onclick => "return confirm('you sure?')"} delete
|
22
|
+
- else
|
23
|
+
site admin
|
@@ -0,0 +1,10 @@
|
|
1
|
+
#sinatra_authentication
|
2
|
+
%h1 Login
|
3
|
+
%form{:action => "/login", :method => "post"}
|
4
|
+
%input{:id => "user_email", :name => "email", :size => 30, :type => "text"}
|
5
|
+
email
|
6
|
+
%br
|
7
|
+
%input{:id => "user_password", :name => "password", :size => 30, :type => "password"}
|
8
|
+
password
|
9
|
+
%br
|
10
|
+
%input{:value => "login", :type => "submit"}
|
data/lib/views/show.haml
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#sinatra_authentication
|
2
|
+
%h1 Signup
|
3
|
+
%form{:action => "/signup", :method => "post"}
|
4
|
+
%input{ :id => "user_email", :name => "user[email]", :size => 30, :type => "text" }
|
5
|
+
email
|
6
|
+
%br
|
7
|
+
%input{ :id => "user_password", :name => "user[password]", :size => 30, :type => "password" }
|
8
|
+
password
|
9
|
+
%br
|
10
|
+
%input{ :id => "user_password_confirmation", :name => "user[password_confirmation]", :size => 30, :type => "password" }
|
11
|
+
confirm
|
12
|
+
%br
|
13
|
+
%input{ :value => "sign up", :type => "submit" }
|
data/readme.markdown
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
### a little sinatra gem that implements user authentication, with support for both datamapper and rufus-tokyo
|
2
|
+
|
3
|
+
## INSTALLATION:
|
4
|
+
|
5
|
+
in your sinatra app simply require either "dm-core" or "rufus-tokyo" and then "sinatra-authentication" and turn on session storage
|
6
|
+
with a super secret key, like so:
|
7
|
+
|
8
|
+
require "dm-core"
|
9
|
+
require "sinatra-authentication"
|
10
|
+
|
11
|
+
use Rack::Session::Cookie, :secret => 'A1 sauce 1s so good you should use 1t on a11 yr st34ksssss'
|
12
|
+
|
13
|
+
If you're using rufus-tokyo, you also need to set the database path for Users. like so:
|
14
|
+
|
15
|
+
require "rufus_tokyo"
|
16
|
+
require "sinatra-authentication"
|
17
|
+
TcUserTable.cabinet_path = File.dirname(__FILE__) + 'folder/where/you/wanna/store/your/database'
|
18
|
+
|
19
|
+
use Rack::Session::Cookie, :secret => 'A1 sauce 1s so good you should use 1t on a11 yr st34ksssss'
|
20
|
+
|
21
|
+
## DEFAULT ROUTES:
|
22
|
+
|
23
|
+
* get '/login'
|
24
|
+
* get '/logout'
|
25
|
+
* get '/signup'
|
26
|
+
* get/post '/users'
|
27
|
+
* get '/users/:id'
|
28
|
+
* get/post '/users/:id/edit'
|
29
|
+
* get '/users/:id/delete'
|
30
|
+
|
31
|
+
If you fetch any of the user pages using ajax, they will automatically render without a layout
|
32
|
+
|
33
|
+
## HELPER METHODS:
|
34
|
+
|
35
|
+
This plugin provides the following helper methods for your sinatra app:
|
36
|
+
|
37
|
+
* login_required
|
38
|
+
> which you place at the beginning of any routes you want to be protected
|
39
|
+
* current_user
|
40
|
+
* logged_in?
|
41
|
+
* render_login_logout(html_attributes)
|
42
|
+
> Which renders login/logout and singup/edit account links.
|
43
|
+
If you pass a hash of html parameters to render_login_logout all the links will get set to them.
|
44
|
+
Which useful for if you're using some sort of lightbox
|
45
|
+
|
46
|
+
## SIMPLE PERMISSIONS:
|
47
|
+
|
48
|
+
By default the user class includes a method called admin? which simply checks
|
49
|
+
if user.permission_level == -1.
|
50
|
+
|
51
|
+
you can take advantage of this method in your views or controllers by calling
|
52
|
+
current_user.admin?
|
53
|
+
i.e.
|
54
|
+
|
55
|
+
- if current_user.admin?
|
56
|
+
%a{:href => "/adminey_link_route_thing"} do something adminey
|
57
|
+
|
58
|
+
(these view examples are in HAML, by the way)
|
59
|
+
|
60
|
+
You can also extend the user class with any convenience methods for determining permissions.
|
61
|
+
i.e.
|
62
|
+
|
63
|
+
#somewhere in the murky depths of your sinatra app
|
64
|
+
class User
|
65
|
+
def peasant?
|
66
|
+
self.permission_level == 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
then in your views you can do
|
71
|
+
|
72
|
+
- if current_user.peasant?
|
73
|
+
%h1 hello peasant!
|
74
|
+
%p Welcome to the caste system! It's very depressing.
|
75
|
+
|
76
|
+
if no one is logged in, current_user returns a GuestUser instance, which responds to current_user.guest?
|
77
|
+
with true, current_user.permission_level with 0 and any other method calls with false
|
78
|
+
|
79
|
+
This makes some view logic easier since you don't always have to check if the user is logged in,
|
80
|
+
although a logged_in? helper method is still provided
|
81
|
+
|
82
|
+
## RUFUS TOKYO
|
83
|
+
|
84
|
+
when using rufus-tokyo, current_user returns a hash, so to get the primary key of the current_user you would do current_user[:pk].
|
85
|
+
if you wanna set an attribute, you can do something like current_user["has_a_dog"] = true
|
86
|
+
and if you want to open a connection with the cabinet directly, you can do something like
|
87
|
+
|
88
|
+
user_connection = TcUser.new
|
89
|
+
users_with_gmail = user_connection.query do |q|
|
90
|
+
q.add 'email', :strinc, 'gmail'
|
91
|
+
end
|
92
|
+
user_connection.close
|
data/test/lib/dm_app.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'haml'
|
4
|
+
require 'dm-core'
|
5
|
+
require File.join(File.dirname(__FILE__), '../../lib/sinatra-authentication')
|
6
|
+
|
7
|
+
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/test.db")
|
8
|
+
DataMapper.auto_migrate!
|
9
|
+
|
10
|
+
use Rack::Session::Cookie, :secret => "heyhihello"
|
11
|
+
|
12
|
+
set :environment, 'development'
|
13
|
+
set :public, 'public'
|
14
|
+
set :views, 'views'
|
15
|
+
|
16
|
+
get '/' do
|
17
|
+
haml "hi", :layout => :layout
|
18
|
+
end
|
data/test/lib/helper.rb
ADDED
data/test/lib/tc_app.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sinatra'
|
3
|
+
require 'haml'
|
4
|
+
require 'rufus/tokyo'
|
5
|
+
require File.join(File.dirname(__FILE__), '../../lib/sinatra-authentication')
|
6
|
+
|
7
|
+
use Rack::Session::Cookie, :secret => "heyhihello"
|
8
|
+
TcUserTable.cabinet_path = File.dirname(__FILE__)
|
9
|
+
|
10
|
+
set :environment, 'development'
|
11
|
+
set :public, 'public'
|
12
|
+
set :views, 'views'
|
13
|
+
|
14
|
+
get '/' do
|
15
|
+
haml "hi", :layout => :layout
|
16
|
+
end
|
data/test/route_tests.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
Test::Unit::TestCase.send :include, Rack::Test::Methods
|
2
|
+
|
3
|
+
class SinatraAuthDataMapperTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
post '/signup', TestHelper.gen_user
|
7
|
+
follow_redirect!
|
8
|
+
get '/logout'
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_should_login
|
12
|
+
post '/login', {'email' => TestHelper.gen_user['user[email]'], 'password' => TestHelper.gen_user['user[password]']}
|
13
|
+
follow_redirect!
|
14
|
+
|
15
|
+
assert_equal 'http://example.org/', last_request.url
|
16
|
+
#assert cookie_jar['user']
|
17
|
+
assert last_request.env['rack.session'][:user]
|
18
|
+
assert last_response.ok?
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_should_logout
|
22
|
+
post '/login', {'email' => TestHelper.gen_user['user[email]'], 'password' => TestHelper.gen_user['user[password]']}
|
23
|
+
get '/logout'
|
24
|
+
follow_redirect!
|
25
|
+
|
26
|
+
assert !last_request.env['rack.session'][:user]
|
27
|
+
assert_equal 'http://example.org/', last_request.url
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-authentication
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Max Justus Spransy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-07 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: sinatra
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: dm-core
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: dm-validations
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: dm-timestamps
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rufus-tokyo
|
57
|
+
type: :runtime
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
description: Simple authentication plugin for sinatra.
|
66
|
+
email: maxjustus@gmail.com
|
67
|
+
executables: []
|
68
|
+
|
69
|
+
extensions: []
|
70
|
+
|
71
|
+
extra_rdoc_files: []
|
72
|
+
|
73
|
+
files:
|
74
|
+
- .gitignore
|
75
|
+
- History.txt
|
76
|
+
- Manifest
|
77
|
+
- Rakefile
|
78
|
+
- TODO
|
79
|
+
- lib/models/abstract_user.rb
|
80
|
+
- lib/models/datamapper_user.rb
|
81
|
+
- lib/models/dm_adapter.rb
|
82
|
+
- lib/models/rufus_tokyo_user.rb
|
83
|
+
- lib/models/tc_adapter.rb
|
84
|
+
- lib/sinatra-authentication.rb
|
85
|
+
- lib/views/edit.haml
|
86
|
+
- lib/views/index.haml
|
87
|
+
- lib/views/login.haml
|
88
|
+
- lib/views/show.haml
|
89
|
+
- lib/views/signup.haml
|
90
|
+
- readme.markdown
|
91
|
+
- test/datamapper_test.rb
|
92
|
+
- test/lib/dm_app.rb
|
93
|
+
- test/lib/helper.rb
|
94
|
+
- test/lib/tc_app.rb
|
95
|
+
- test/route_tests.rb
|
96
|
+
- test/rufus_tokyo_test.rb
|
97
|
+
has_rdoc: true
|
98
|
+
homepage: http://github.com/maxjustus/sinatra-authentication
|
99
|
+
licenses: []
|
100
|
+
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options:
|
103
|
+
- --charset=UTF-8
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: "0"
|
111
|
+
version:
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: "0"
|
117
|
+
version:
|
118
|
+
requirements: []
|
119
|
+
|
120
|
+
rubyforge_project:
|
121
|
+
rubygems_version: 1.3.5
|
122
|
+
signing_key:
|
123
|
+
specification_version: 3
|
124
|
+
summary: Simple authentication plugin for sinatra.
|
125
|
+
test_files:
|
126
|
+
- test/lib/tc_app.rb
|
127
|
+
- test/lib/helper.rb
|
128
|
+
- test/lib/dm_app.rb
|
129
|
+
- test/datamapper_test.rb
|
130
|
+
- test/rufus_tokyo_test.rb
|
131
|
+
- test/route_tests.rb
|