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 ADDED
@@ -0,0 +1,4 @@
1
+ pkg/
2
+ *.swp
3
+ *.db
4
+ *.tct
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-04-06
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
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
@@ -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"}
@@ -0,0 +1,6 @@
1
+ #sinatra_authentication
2
+ %h1= @user.email
3
+ - current_user.admin?
4
+ - if current_user.admin?
5
+ %h2 permission level
6
+ = @user.permission_level
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'lib/dm_app'
2
+ require 'lib/helper'
3
+ require 'test/unit'
4
+ require 'rack/test'
5
+ require 'route_tests'
@@ -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
@@ -0,0 +1,9 @@
1
+ class TestHelper
2
+ def self.gen_user
3
+ {'user[email]' => 'yodawg@gmail.com', 'user[password]' => 'password', 'user[password_confirmation]' => 'password'}
4
+ end
5
+ end
6
+
7
+ def app
8
+ Sinatra::Application
9
+ end
@@ -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
@@ -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
@@ -0,0 +1,5 @@
1
+ require 'lib/tc_app'
2
+ require 'lib/helper'
3
+ require 'test/unit'
4
+ require 'rack/test'
5
+ require 'route_tests'
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