sinatra-authentication 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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