sinatra-authentication 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/Rakefile +6 -1
  2. data/lib/models/abstract_user.rb +39 -1
  3. data/lib/models/datamapper_user.rb +2 -1
  4. data/lib/models/dm_adapter.rb +15 -4
  5. data/lib/models/mm_adapter.rb +21 -9
  6. data/lib/models/mongoid_adapter.rb +67 -0
  7. data/lib/models/mongoid_user.rb +41 -0
  8. data/lib/models/mongomapper_user.rb +4 -8
  9. data/lib/models/rufus_tokyo_user.rb +48 -19
  10. data/lib/models/sequel_adapter.rb +68 -0
  11. data/lib/models/sequel_user.rb +53 -0
  12. data/lib/models/tc_adapter.rb +20 -2
  13. data/lib/sinatra-authentication.rb +35 -23
  14. data/lib/sinatra-authentication/models.rb +5 -0
  15. data/lib/views/edit.haml +1 -1
  16. data/lib/views/login.haml +3 -3
  17. data/lib/views/signup.haml +4 -4
  18. data/readme.markdown +53 -3
  19. data/sinatra-authentication-0.3.2.gem +0 -0
  20. data/sinatra-authentication.gemspec +86 -60
  21. data/spec/run_all_specs.rb +8 -0
  22. data/spec/unit/dm_model_spec.rb +3 -0
  23. data/spec/unit/mm_model_spec.rb +3 -0
  24. data/spec/unit/mongoid_model_spec.rb +3 -0
  25. data/spec/unit/sequel_model_spec.rb +10 -0
  26. data/spec/unit/tc_model_spec.rb +3 -0
  27. data/spec/unit/user_specs.rb +119 -0
  28. data/test/lib/dm_app.rb +1 -0
  29. data/test/lib/dm_extend_app.rb +1 -0
  30. data/test/lib/extend_views/edit.haml +4 -0
  31. data/test/lib/extend_views/signup.haml +4 -4
  32. data/test/lib/helper.rb +4 -0
  33. data/test/lib/mm_app.rb +16 -15
  34. data/test/lib/mongoid_app.rb +29 -0
  35. data/test/lib/sequel_app.rb +22 -0
  36. data/test/lib/tc_app.rb +2 -0
  37. data/test/mongoid_test.rb +5 -0
  38. data/test/mongomapper_test.rb +36 -35
  39. data/test/sequel_test.rb +5 -0
  40. metadata +40 -24
  41. data/.gitignore +0 -4
@@ -0,0 +1,68 @@
1
+ module SequelAdapter
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ base.class_eval { include SequelAdapter::InstanceMethods }
5
+ end
6
+
7
+ module ClassMethods
8
+ def all
9
+ result = SequelUser.order(:created_at.desc).all
10
+ result.collect {|instance| self.new instance}
11
+ end
12
+
13
+ def get(hash)
14
+ if user = SequelUser.first(hash)
15
+ self.new user
16
+ else
17
+ nil
18
+ end
19
+ end
20
+
21
+ def set(attributes)
22
+ user = SequelUser.new attributes
23
+ if user.valid?
24
+ user.save
25
+ #false
26
+ end
27
+
28
+ self.new user
29
+ end
30
+
31
+ def set!(attributes)
32
+ user = SequelUser.new attributes
33
+ user.save!
34
+ self.new user
35
+ end
36
+
37
+ def delete(pk)
38
+ user = SequelUser.first(:id => pk)
39
+ user.destroy
40
+ !user.exists?
41
+ end
42
+ end
43
+
44
+ module InstanceMethods
45
+ def errors
46
+ @instance.errors.full_messages.join(', ')
47
+ end
48
+
49
+ def valid
50
+ @instance.valid?
51
+ end
52
+
53
+ def update(attributes)
54
+ @instance.set attributes
55
+ if @instance.valid?
56
+ @instance.save
57
+ true
58
+ else
59
+ false
60
+ end
61
+ end
62
+
63
+ def method_missing(meth, *args, &block)
64
+ #cool I just found out * on an array turns the array into a list of args for a function
65
+ @instance.send(meth, *args, &block)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,53 @@
1
+ unless DB.table_exists? :sequel_users
2
+ DB.create_table :sequel_users do
3
+ primary_key :id
4
+ String :email, :unique => true, :text => true
5
+ String :hashed_password
6
+ String :salt
7
+ DateTime :created_at
8
+ Integer :permission_level, :default => 1
9
+ if Sinatra.const_defined?('FacebookObject')
10
+ String :fb_uid
11
+ end
12
+
13
+ #check{{char_length(email)=>5..40}}
14
+ end
15
+ end
16
+
17
+ class SequelUser < Sequel::Model
18
+ attr_writer :password_confirmation
19
+ plugin :validation_helpers
20
+ plugin :timestamps, :create => :created_at
21
+
22
+ def validate
23
+ super
24
+ email_regexp = /(\A(\s*)\Z)|(\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z)/i
25
+ validates_format email_regexp, :email
26
+ validates_presence :email
27
+ validates_unique :email
28
+ validates_presence :password if new?
29
+ errors.add :passwords, ' don\'t match' unless @password == @password_confirmation
30
+ #validate equality?
31
+ end
32
+ #TODO validate format of email
33
+
34
+ def password=(pass)
35
+ @password = pass
36
+ self.salt = User.random_string(10) if !self.salt
37
+ self.hashed_password = User.encrypt(@password, self.salt)
38
+ end
39
+
40
+ def admin?
41
+ self.permission_level == -1 || self.id == 1
42
+ end
43
+
44
+ def site_admin?
45
+ self.id == 1
46
+ end
47
+
48
+ protected
49
+
50
+ def method_missing(m, *args)
51
+ return false
52
+ end
53
+ end
@@ -21,7 +21,11 @@ module TcAdapter
21
21
  def get(hash)
22
22
  if hash[:id]
23
23
  pk = hash[:id]
24
- result = TcUser.get(pk)
24
+ if pk.length > 0
25
+ result = TcUser.get(pk)
26
+ else
27
+ nil
28
+ end
25
29
  else
26
30
  result = TcUser.query do |q|
27
31
  hash.each do |key, value|
@@ -51,7 +55,13 @@ module TcAdapter
51
55
  if user == [] #no user
52
56
  self.new TcUser.set(attributes)
53
57
  else
54
- false
58
+ if attributes['email'].length == 0
59
+ error = 'You need to provide an email address'
60
+ else
61
+ error = 'That email is already taken'
62
+ end
63
+
64
+ TcUser.new(attributes, error)
55
65
  end
56
66
  end
57
67
 
@@ -70,6 +80,14 @@ module TcAdapter
70
80
  @instance.update attributes
71
81
  end
72
82
 
83
+ def errors
84
+ @instance.errors.join(', ')
85
+ end
86
+
87
+ def valid
88
+ @instance.errors.length == 0
89
+ end
90
+
73
91
  def method_missing(meth, *args, &block)
74
92
  #cool I just found out * on an array turn the array into a list of args for a function
75
93
  @instance.send(meth, *args, &block)
@@ -3,7 +3,7 @@ require 'pathname'
3
3
  require Pathname(__FILE__).dirname.expand_path + "models/abstract_user"
4
4
 
5
5
  module Sinatra
6
- module LilAuthentication
6
+ module SinatraAuthentication
7
7
  def self.registered(app)
8
8
  #INVESTIGATE
9
9
  #the possibility of sinatra having an array of view_paths to load from
@@ -11,9 +11,9 @@ module Sinatra
11
11
  #sinatra 9.1.1 doesn't have multiple view capability anywhere
12
12
  #so to get around I have to do it totally manually by
13
13
  #loading the view from this path into a string and rendering it
14
- set :sinatra_authentication_view_path, Pathname(__FILE__).dirname.expand_path + "views/"
14
+ app.set :sinatra_authentication_view_path, Pathname(__FILE__).dirname.expand_path + "views/"
15
15
 
16
- get '/users' do
16
+ app.get '/users' do
17
17
  login_required
18
18
  redirect "/" unless current_user.admin?
19
19
 
@@ -25,7 +25,7 @@ module Sinatra
25
25
  end
26
26
  end
27
27
 
28
- get '/users/:id' do
28
+ app.get '/users/:id' do
29
29
  login_required
30
30
 
31
31
  @user = User.get(:id => params[:id])
@@ -33,7 +33,7 @@ module Sinatra
33
33
  end
34
34
 
35
35
  #convenience for ajax but maybe entirely stupid and unnecesary
36
- get '/logged_in' do
36
+ app.get '/logged_in' do
37
37
  if session[:user]
38
38
  "true"
39
39
  else
@@ -41,11 +41,15 @@ module Sinatra
41
41
  end
42
42
  end
43
43
 
44
- get '/login' do
45
- haml get_view_as_string("login.haml"), :layout => use_layout?
44
+ app.get '/login' do
45
+ if session[:user]
46
+ redirect '/'
47
+ else
48
+ haml get_view_as_string("login.haml"), :layout => use_layout?
49
+ end
46
50
  end
47
51
 
48
- post '/login' do
52
+ app.post '/login' do
49
53
  if user = User.authenticate(params[:email], params[:password])
50
54
  session[:user] = user.id
51
55
 
@@ -68,7 +72,7 @@ module Sinatra
68
72
  end
69
73
  end
70
74
 
71
- get '/logout' do
75
+ app.get '/logout' do
72
76
  session[:user] = nil
73
77
  if Rack.const_defined?('Flash')
74
78
  flash[:notice] = "Logout successful."
@@ -76,13 +80,17 @@ module Sinatra
76
80
  redirect '/'
77
81
  end
78
82
 
79
- get '/signup' do
80
- haml get_view_as_string("signup.haml"), :layout => use_layout?
83
+ app.get '/signup' do
84
+ if session[:user]
85
+ redirect '/'
86
+ else
87
+ haml get_view_as_string("signup.haml"), :layout => use_layout?
88
+ end
81
89
  end
82
90
 
83
- post '/signup' do
91
+ app.post '/signup' do
84
92
  @user = User.set(params[:user])
85
- if @user && @user.id
93
+ if @user.valid && @user.id
86
94
  session[:user] = @user.id
87
95
  if Rack.const_defined?('Flash')
88
96
  flash[:notice] = "Account created."
@@ -90,20 +98,20 @@ module Sinatra
90
98
  redirect '/'
91
99
  else
92
100
  if Rack.const_defined?('Flash')
93
- flash[:notice] = 'There were some problems creating your account. Please be sure you\'ve entered all your information correctly.'
101
+ flash[:notice] = "There were some problems creating your account: #{@user.errors}."
94
102
  end
95
- redirect '/signup'
103
+ redirect '/signup?' + hash_to_query_string(params['user'])
96
104
  end
97
105
  end
98
106
 
99
- get '/users/:id/edit' do
107
+ app.get '/users/:id/edit' do
100
108
  login_required
101
109
  redirect "/users" unless current_user.admin? || current_user.id.to_s == params[:id]
102
110
  @user = User.get(:id => params[:id])
103
111
  haml get_view_as_string("edit.haml"), :layout => use_layout?
104
112
  end
105
113
 
106
- post '/users/:id/edit' do
114
+ app.post '/users/:id/edit' do
107
115
  login_required
108
116
  redirect "/users" unless current_user.admin? || current_user.id.to_s == params[:id]
109
117
 
@@ -121,13 +129,13 @@ module Sinatra
121
129
  redirect '/'
122
130
  else
123
131
  if Rack.const_defined?('Flash')
124
- flash[:notice] = 'Whoops, looks like there were some problems with your updates.'
132
+ flash[:notice] = "Whoops, looks like there were some problems with your updates: #{user.errors}."
125
133
  end
126
- redirect "/users/#{user.id}/edit"
134
+ redirect "/users/#{user.id}/edit?" + hash_to_query_string(user_attributes)
127
135
  end
128
136
  end
129
137
 
130
- get '/users/:id/delete' do
138
+ app.get '/users/:id/delete' do
131
139
  login_required
132
140
  redirect "/users" unless current_user.admin? || current_user.id.to_s == params[:id]
133
141
 
@@ -145,7 +153,7 @@ module Sinatra
145
153
 
146
154
 
147
155
  if Sinatra.const_defined?('FacebookObject')
148
- get '/connect' do
156
+ app.get '/connect' do
149
157
  if fb[:user]
150
158
  if current_user.class != GuestUser
151
159
  user = current_user
@@ -166,7 +174,7 @@ module Sinatra
166
174
  redirect '/'
167
175
  end
168
176
 
169
- get '/receiver' do
177
+ app.get '/receiver' do
170
178
  %[<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
171
179
  <html xmlns="http://www.w3.org/1999/xhtml" >
172
180
  <body>
@@ -179,6 +187,10 @@ module Sinatra
179
187
  end
180
188
 
181
189
  module Helpers
190
+ def hash_to_query_string(hash)
191
+ hash.collect {|k,v| "#{k}=#{v}"}.join('&')
192
+ end
193
+
182
194
  def login_required
183
195
  #not as efficient as checking the session. but this inits the fb_user if they are logged in
184
196
  if current_user.class != GuestUser
@@ -270,7 +282,7 @@ module Sinatra
270
282
  end
271
283
  end
272
284
 
273
- register LilAuthentication
285
+ register SinatraAuthentication
274
286
  end
275
287
 
276
288
  class GuestUser
@@ -0,0 +1,5 @@
1
+ # Allows you to use the models from cron scripts etc
2
+ if !Object.const_defined?("Sinatra")
3
+ class Sinatra;end # To please the testing for Sinatra.const_defined?('FacebookObject')
4
+ require File.expand_path(File.join(__FILE__,'../../models/') + 'abstract_user.rb')
5
+ end
@@ -1,5 +1,5 @@
1
1
  #sinatra_authentication
2
- - if Rack.const_defined?('Flash')
2
+ - if Rack.const_defined?('Flash') && flash[:notice]
3
3
  #sinatra_authentication_flash= flash[:notice]
4
4
  %h1
5
5
  Edit
@@ -1,5 +1,5 @@
1
1
  #sinatra_authentication
2
- - if Rack.const_defined?('Flash')
2
+ - if Rack.const_defined?('Flash') && flash[:notice]
3
3
  #sinatra_authentication_flash= flash[:notice]
4
4
  %h1.page_title Login
5
5
  %form{:action => "/login", :method => "post"}
@@ -12,9 +12,9 @@
12
12
  %label{:for => "user_password"} Password
13
13
  %input{:id => "user_password", :name => "password", :size => 30, :type => "password"}
14
14
  .buttons
15
- %input{:value => "login", :type => "submit"}
15
+ %input{:value => "Login", :type => "submit"}
16
16
  %a{:href => "/signup", :class => 'sinatra_authentication_link'}
17
- Signup
17
+ Sign up
18
18
  - if Sinatra.const_defined?('FacebookObject')
19
19
  .third_party_signup
20
20
  %h3.section_title One click login:
@@ -1,12 +1,12 @@
1
1
  #sinatra_authentication
2
- - if Rack.const_defined?('Flash')
2
+ - if Rack.const_defined?('Flash') && flash[:notice]
3
3
  #sinatra_authentication_flash= flash[:notice]
4
- %h1.page_title Signup
4
+ %h1.page_title Sign Up
5
5
  %form{:action => "/signup", :method => "post"}
6
6
  .field
7
7
  .label
8
8
  %label{:for => "user_email"} Email
9
- %input{ :id => "user_email", :name => "user[email]", :size => 30, :type => "text" }
9
+ %input{ :id => "user_email", :name => "user[email]", :size => 30, :type => "text" , :value => params[:email]}
10
10
  .field
11
11
  .label
12
12
  %label{:for => "user_password"} Password
@@ -16,7 +16,7 @@
16
16
  %label{:for => "user_password_confirmation"} Confirm Password
17
17
  %input{ :id => "user_password_confirmation", :name => "user[password_confirmation]", :size => 30, :type => "password" }
18
18
  .buttons
19
- %input{ :value => "Create account", :type => "submit" }
19
+ %input{ :value => "Sign up", :type => "submit" }
20
20
  %a{:href => "/login", :class => 'sinatra_authentication_link'}
21
21
  Login
22
22
  - if Sinatra.const_defined?('FacebookObject')
@@ -1,8 +1,8 @@
1
- ### A little sinatra gem that implements user authentication, with support for datamapper, mongomapper and rufus-tokyo
1
+ ### A little sinatra gem that implements user authentication, with support for Datamapper, Mongomapper, Mongoid, Sequel and Rufus-Tokyo
2
2
 
3
3
  ## INSTALLATION:
4
4
 
5
- in your sinatra app simply require either "dm-core", "rufus-tokyo" or "mongo_mapper", "digest/sha1", 'rack-flash' (if you want flash messages) and then "sinatra-authentication" and turn on session storage
5
+ in your sinatra app simply require either "dm-core", 'sequel', 'rufus-tokyo', 'mongoid' or "mongo_mapper", "digest/sha1", 'rack-flash' (if you want flash messages) and then "sinatra-authentication" and turn on session storage
6
6
  with a super secret key, like so:
7
7
 
8
8
  require "dm-core"
@@ -226,13 +226,63 @@ And then to access/update your newly defined attributes you use the User class:
226
226
  user.delete
227
227
  end
228
228
 
229
- the User class passes additional method calls along to the interfacing database class, so calls to Datamapper/Mongomapper/RufusTokyo functions should work as expected.
229
+ the User class passes additional method calls along to the interfacing database class, so most calls to Datamapper/Sequel/Mongomapper/RufusTokyo functions should work as expected.
230
+
231
+ If you need to get associations on current_user from the underlying ORM use current_user.db_instance, take this case for example:
232
+ class Brain
233
+ include DataMapper::Resource
234
+ property :type, String
235
+ property :iq, Integer
236
+ end
237
+
238
+ class DmUser
239
+ has n, :brains
240
+ end
241
+
242
+ get '/' do
243
+ @user_brains = current_user.db_instance.brains
244
+ end
230
245
 
231
246
  The database user classes are named as follows:
232
247
 
233
248
  * for Datamapper:
234
249
  > DmUser
250
+ * for Sequel:
251
+ > SequelUser
235
252
  * for Rufus Tokyo:
236
253
  > TcUser
254
+ * for Mongoid:
255
+ > MongoidUser
237
256
  * for Mongomapper:
238
257
  > MmUser
258
+
259
+ ## Deprecations
260
+ * All database adapters now store created_at as a Time object.
261
+
262
+ ## Known issues
263
+ * First user in database is not properly recognized as site admin
264
+ > Proposed fix: add site_admin_email option when initialization functionality is added
265
+
266
+ ## Roadmap
267
+
268
+ * Move database adapter initialization, along with auto configuration of sinbook and rack flash functionality into a Sinatra::SinatraAuthentication.init(args) method
269
+ * Refactor/redesign database adapter interface, make User class AbstractUser and all ORM user classes User, with corresponding specs
270
+ * Remove Facebook connect support and add support for Omniauth
271
+ * Provide a method for overriding specific views, and/or specifying your own form partial, (passed an instance of User)
272
+ * Add Remember me (forever) checkbox to login form
273
+ * Add next url parameter support for login/signup
274
+ * Add verb selection on configuration (Sign in / Log in)
275
+ * Provide optional support through init method for inclusion of username
276
+ > Where login form accepts either email or username (through the same field)
277
+ * Add email functionality
278
+ > Confirmation emails
279
+ > Forgotten password emails
280
+ * Look into what might be neccesary to allow for logging in using Ajax
281
+
282
+ ## Maybe
283
+
284
+ * Allow passing custom database attributes into init method, also dynamically altering corresponding signup and user edit views. (potentially leaky abstraction)
285
+ > As an alternative, create a generic interface for accessing database row names through the various ORMs.
286
+ > So when users alter their User schemas, I can make my views 'Just Work'.
287
+ * Add HTTP basic auth support
288
+ * Add pluggable OAuth consumer/provider support