sinatra-doorman 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +80 -0
- data/Rakefile +98 -0
- data/lib/doorman.rb +7 -0
- data/lib/doorman/base.rb +271 -0
- data/lib/doorman/messages.rb +32 -0
- data/lib/doorman/user.rb +103 -0
- data/lib/rack/contrib/cookies.rb +50 -0
- data/views/forgot.haml +4 -0
- data/views/login.haml +9 -0
- data/views/reset.haml +7 -0
- data/views/signup.haml +10 -0
- metadata +166 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Nick Plante
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
= Sinatra Doorman
|
2
|
+
|
3
|
+
A user authentication extension for Sinatra based on Warden.
|
4
|
+
|
5
|
+
== Features
|
6
|
+
|
7
|
+
Base
|
8
|
+
* Signup w/ email confirmation
|
9
|
+
* Login/Logout
|
10
|
+
|
11
|
+
Optional
|
12
|
+
* Remember Me
|
13
|
+
* Forgotten password reset
|
14
|
+
|
15
|
+
== Installation
|
16
|
+
|
17
|
+
This project requires Sinatra 1.0 ({see #297}[https://sinatra.lighthouseapp.com/projects/9779/tickets/297-sinatra-extension-routes-are-not-available-in-app]).
|
18
|
+
Currently the user model requires DataMapper, this dependency may be removed
|
19
|
+
in the future.
|
20
|
+
|
21
|
+
gem install sinatra --pre
|
22
|
+
gem install warden pony dm-core dm-validations dm-timestamps
|
23
|
+
gem install sinatra-doorman
|
24
|
+
|
25
|
+
== Usage
|
26
|
+
|
27
|
+
require 'doorman'
|
28
|
+
|
29
|
+
use Rack::Session::Cookie
|
30
|
+
|
31
|
+
#Optional, if you want user notices
|
32
|
+
require 'rack/flash'
|
33
|
+
use Rack::Flash
|
34
|
+
|
35
|
+
To use as a middleware
|
36
|
+
|
37
|
+
use Sinatra::Doorman::Middleware
|
38
|
+
|
39
|
+
To use as a Sinatra extension, call register on the features you want
|
40
|
+
|
41
|
+
#call Sinatra.register if you are writing a top-level app
|
42
|
+
register Sinatra::Doorman::Base
|
43
|
+
register Sinatra::Doorman::RememberMe
|
44
|
+
register Sinatra::Doorman::ForgotPassword
|
45
|
+
|
46
|
+
Note: usually you don't need to call register explicitly when extending 'classic'
|
47
|
+
top-level Sinatra apps, because the extension author will call it for you.
|
48
|
+
This is not the case in this project, because I wanted to keep some components
|
49
|
+
optional, and I did not want to alter the top-level namespace of any Sinatra
|
50
|
+
apps using this as middleware.
|
51
|
+
|
52
|
+
== Views
|
53
|
+
|
54
|
+
At this time, you need to copy the contents of the views folder in this project
|
55
|
+
to the views folder of your Sinatra application.
|
56
|
+
|
57
|
+
It is my objective to make this middleware useful for any Rack based application.
|
58
|
+
One approach that I have been considering is requesting an empty layout from
|
59
|
+
the downstream app, and transforming it using {Effigy}[http://github.com/jferris/effigy].
|
60
|
+
|
61
|
+
Ideally I would like:
|
62
|
+
* Reasonable default views without copying files from GH or gem
|
63
|
+
* User option to replace/customize default views
|
64
|
+
* Rendering within the application layout
|
65
|
+
|
66
|
+
I can not think of any middleware that adds to or significantly alters an app's
|
67
|
+
response body. Perhaps this is not an appropriate thing to be doing, but it makes
|
68
|
+
sense to me on some level. Ultimately, I would like this code to be something that
|
69
|
+
one could plug into their middleware stack and use without too much trouble.
|
70
|
+
|
71
|
+
== Development and Testing
|
72
|
+
|
73
|
+
If you want to work on this project, there is one thing to note.
|
74
|
+
When I run all the cucumber features at once, I get some kind of issue with Webrat
|
75
|
+
where Infinite Redirect errors are raised on the signup feature. From what I can
|
76
|
+
tell, there is not actually any infinite redirect problem. I have not got
|
77
|
+
to the bottom of this yet, so in the meantime I run cucumber in two bites:
|
78
|
+
|
79
|
+
cucumber --tags @signup
|
80
|
+
cucumber --tags ~@signup
|
data/Rakefile
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
task :default => :test
|
2
|
+
task :test => [:spec, :cucumber]
|
3
|
+
|
4
|
+
namespace :db do
|
5
|
+
desc 'Auto-migrate the database (destroys data)'
|
6
|
+
task :migrate do
|
7
|
+
require 'application'
|
8
|
+
DataMapper.auto_migrate!
|
9
|
+
end
|
10
|
+
|
11
|
+
desc 'Auto-upgrade the database (preserves data)'
|
12
|
+
task :upgrade do
|
13
|
+
require 'application'
|
14
|
+
DataMapper.auto_upgrade!
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
require 'spec/rake/spectask'
|
19
|
+
desc "Run specs"
|
20
|
+
Spec::Rake::SpecTask.new do |t|
|
21
|
+
t.spec_files = FileList['spec/**/*.rb']
|
22
|
+
t.spec_opts = ['-cfs']
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'cucumber/rake/task'
|
26
|
+
desc "Run cucumber features"
|
27
|
+
Cucumber::Rake::Task.new do |t|
|
28
|
+
t.cucumber_opts = '--format pretty'
|
29
|
+
end
|
30
|
+
|
31
|
+
require "rake/gempackagetask"
|
32
|
+
require "rake/rdoctask"
|
33
|
+
|
34
|
+
# This builds the actual gem. For details of what all these options
|
35
|
+
# mean, and other ones you can add, check the documentation here:
|
36
|
+
#
|
37
|
+
# http://rubygems.org/read/chapter/20
|
38
|
+
#
|
39
|
+
spec = Gem::Specification.new do |s|
|
40
|
+
|
41
|
+
# Change these as appropriate
|
42
|
+
s.name = "sinatra-doorman"
|
43
|
+
s.version = "0.1.0"
|
44
|
+
s.summary = "A user authentication middleware built with Sinatra and Warden"
|
45
|
+
s.author = "John Mendonca"
|
46
|
+
s.email = "joaosinho@gmail.com"
|
47
|
+
s.homepage = "http://github.com/johnmendonca/sinatra-doorman"
|
48
|
+
|
49
|
+
s.has_rdoc = true
|
50
|
+
s.extra_rdoc_files = %w(README.rdoc)
|
51
|
+
s.rdoc_options = %w(--main README.rdoc)
|
52
|
+
|
53
|
+
# Add any extra files to include in the gem
|
54
|
+
s.files = %w(MIT-LICENSE Rakefile README.rdoc) + Dir["views/**/*"] + Dir["lib/**/*"]
|
55
|
+
s.require_paths = ["lib"]
|
56
|
+
|
57
|
+
# If you want to depend on other gems, add them here, along with any
|
58
|
+
# relevant versions
|
59
|
+
s.add_dependency("sinatra", "~> 1.0.a")
|
60
|
+
s.add_dependency("warden", "~> 0.9.0")
|
61
|
+
s.add_dependency("pony")
|
62
|
+
s.add_dependency("dm-core", "~> 0.10.2")
|
63
|
+
s.add_dependency("dm-validations", "~> 0.10.2")
|
64
|
+
s.add_dependency("dm-timestamps", "~> 0.10.2")
|
65
|
+
|
66
|
+
# If your tests use any gems, include them here
|
67
|
+
s.add_development_dependency("rspec")
|
68
|
+
s.add_development_dependency("cucumber")
|
69
|
+
s.add_development_dependency("webrat")
|
70
|
+
s.add_development_dependency("rack-test")
|
71
|
+
end
|
72
|
+
|
73
|
+
# This task actually builds the gem. We also regenerate a static
|
74
|
+
# .gemspec file, which is useful if something (i.e. GitHub) will
|
75
|
+
# be automatically building a gem for this project. If you're not
|
76
|
+
# using GitHub, edit as appropriate.
|
77
|
+
#
|
78
|
+
# To publish your gem online, install the 'gemcutter' gem; Read more
|
79
|
+
# about that here: http://gemcutter.org/pages/gem_docs
|
80
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
81
|
+
pkg.gem_spec = spec
|
82
|
+
|
83
|
+
# Generate the gemspec file for github.
|
84
|
+
file = File.dirname(__FILE__) + "/#{spec.name}.gemspec"
|
85
|
+
File.open(file, "w") {|f| f << spec.to_ruby }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Generate documentation
|
89
|
+
Rake::RDocTask.new do |rd|
|
90
|
+
rd.main = "README.rdoc"
|
91
|
+
rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
92
|
+
rd.rdoc_dir = "rdoc"
|
93
|
+
end
|
94
|
+
|
95
|
+
desc 'Clear out RDoc and generated packages'
|
96
|
+
task :clean => [:clobber_rdoc, :clobber_package] do
|
97
|
+
rm "#{spec.name}.gemspec"
|
98
|
+
end
|
data/lib/doorman.rb
ADDED
data/lib/doorman/base.rb
ADDED
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'warden'
|
3
|
+
require 'pony'
|
4
|
+
|
5
|
+
module Sinatra
|
6
|
+
module Doorman
|
7
|
+
|
8
|
+
class Warden::SessionSerializer
|
9
|
+
def serialize(user); user.id; end
|
10
|
+
def deserialize(id); User.get(id); end
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Base Features:
|
15
|
+
# * Signup with Email Confirmation
|
16
|
+
# * Login/Logout
|
17
|
+
##
|
18
|
+
|
19
|
+
class PasswordStrategy < Warden::Strategies::Base
|
20
|
+
def valid?
|
21
|
+
params['user'] &&
|
22
|
+
params['user']['login'] &&
|
23
|
+
params['user']['password']
|
24
|
+
end
|
25
|
+
|
26
|
+
def authenticate!
|
27
|
+
user = User.authenticate(
|
28
|
+
params['user']['login'],
|
29
|
+
params['user']['password'])
|
30
|
+
|
31
|
+
if user.nil?
|
32
|
+
fail!(:login_bad_credentials)
|
33
|
+
elsif !user.confirmed
|
34
|
+
fail!(:login_not_confirmed)
|
35
|
+
else
|
36
|
+
success!(user)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module Base
|
42
|
+
module Helpers
|
43
|
+
def authenticated?
|
44
|
+
env['warden'].authenticated?
|
45
|
+
end
|
46
|
+
alias_method :logged_in?, :authenticated?
|
47
|
+
|
48
|
+
def notify(type, message)
|
49
|
+
message = Messages[message] if message.is_a?(Symbol)
|
50
|
+
flash[type] = message if defined?(Rack::Flash)
|
51
|
+
end
|
52
|
+
|
53
|
+
def token_link(type, user)
|
54
|
+
"http://#{env['HTTP_HOST']}/#{type}/#{user.confirm_token}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.registered(app)
|
59
|
+
app.helpers Helpers
|
60
|
+
|
61
|
+
app.use Warden::Manager do |manager|
|
62
|
+
manager.failure_app = lambda { |env|
|
63
|
+
env['x-rack.flash'][:error] = Messages[:auth_required] if defined?(Rack::Flash)
|
64
|
+
[302, { 'Location' => '/login' }, ['']]
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
Warden::Strategies.add(:password, PasswordStrategy)
|
69
|
+
|
70
|
+
app.get '/signup/?' do
|
71
|
+
redirect '/home' if authenticated?
|
72
|
+
haml :signup
|
73
|
+
end
|
74
|
+
|
75
|
+
app.post '/signup' do
|
76
|
+
redirect '/home' if authenticated?
|
77
|
+
|
78
|
+
user = User.new(params[:user])
|
79
|
+
|
80
|
+
unless user.save
|
81
|
+
notify :error, user.errors.first
|
82
|
+
redirect back
|
83
|
+
end
|
84
|
+
|
85
|
+
notify :success, :signup_success
|
86
|
+
notify :success, 'Signed up: ' + user.confirm_token
|
87
|
+
Pony.mail(
|
88
|
+
:to => user.email,
|
89
|
+
:from => "no-reply@#{env['SERVER_NAME']}",
|
90
|
+
:body => token_link('confirm', user))
|
91
|
+
redirect "/"
|
92
|
+
end
|
93
|
+
|
94
|
+
app.get '/confirm/:token/?' do
|
95
|
+
redirect '/home' if authenticated?
|
96
|
+
|
97
|
+
if params[:token].nil? || params[:token].empty?
|
98
|
+
notify :error, :confirm_no_token
|
99
|
+
redirect '/'
|
100
|
+
end
|
101
|
+
|
102
|
+
user = User.first(:confirm_token => params[:token])
|
103
|
+
if user.nil?
|
104
|
+
notify :error, :confirm_no_user
|
105
|
+
else
|
106
|
+
user.confirm_email!
|
107
|
+
notify :success, :confirm_success
|
108
|
+
end
|
109
|
+
redirect '/login'
|
110
|
+
end
|
111
|
+
|
112
|
+
app.get '/login/?' do
|
113
|
+
redirect '/home' if authenticated?
|
114
|
+
haml :login
|
115
|
+
end
|
116
|
+
|
117
|
+
app.post '/login' do
|
118
|
+
env['warden'].authenticate(:password)
|
119
|
+
redirect '/home' if authenticated?
|
120
|
+
notify :error, env['warden'].message
|
121
|
+
redirect back
|
122
|
+
end
|
123
|
+
|
124
|
+
app.get '/logout/?' do
|
125
|
+
env['warden'].logout(:default)
|
126
|
+
notify :success, :logout_success
|
127
|
+
redirect '/login'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
##
|
133
|
+
# Remember Me Feature
|
134
|
+
##
|
135
|
+
|
136
|
+
COOKIE_KEY = "sinatra.doorman.remember"
|
137
|
+
|
138
|
+
class RememberMeStrategy < Warden::Strategies::Base
|
139
|
+
def valid?
|
140
|
+
!!env['rack.cookies'][COOKIE_KEY]
|
141
|
+
end
|
142
|
+
|
143
|
+
def authenticate!
|
144
|
+
token = env['rack.cookies'][COOKIE_KEY]
|
145
|
+
return unless token
|
146
|
+
user = User.first(:remember_token => token)
|
147
|
+
env['rack.cookies'].delete(COOKIE_KEY) and return if user.nil?
|
148
|
+
success!(user)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
module RememberMe
|
153
|
+
def self.registered(app)
|
154
|
+
app.use Rack::Cookies
|
155
|
+
|
156
|
+
Warden::Strategies.add(:remember_me, RememberMeStrategy)
|
157
|
+
|
158
|
+
app.before do
|
159
|
+
env['warden'].authenticate(:remember_me)
|
160
|
+
end
|
161
|
+
|
162
|
+
Warden::Manager.after_authentication do |user, auth, opts|
|
163
|
+
if auth.winning_strategy.is_a?(RememberMeStrategy) ||
|
164
|
+
(auth.winning_strategy.is_a?(PasswordStrategy) &&
|
165
|
+
auth.params['user']['remember_me'])
|
166
|
+
user.remember_me! # new token
|
167
|
+
auth.env['rack.cookies'][COOKIE_KEY] = {
|
168
|
+
:value => user.remember_token,
|
169
|
+
:expires => Time.now + 7 * 24 * 3600,
|
170
|
+
:path => '/' }
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
Warden::Manager.before_logout do |user, auth, opts|
|
175
|
+
user.forget_me! if user
|
176
|
+
auth.env['rack.cookies'].delete(COOKIE_KEY)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Forgot Password Feature
|
183
|
+
##
|
184
|
+
|
185
|
+
module ForgotPassword
|
186
|
+
def self.registered(app)
|
187
|
+
Warden::Manager.after_authentication do |user, auth, opts|
|
188
|
+
# If the user requested a new password,
|
189
|
+
# but then remembers and logs in,
|
190
|
+
# then invalidate password reset token
|
191
|
+
if auth.winning_strategy.is_a?(PasswordStrategy)
|
192
|
+
user.remembered_password!
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
app.get '/forgot/?' do
|
197
|
+
redirect '/home' if authenticated?
|
198
|
+
haml :forgot
|
199
|
+
end
|
200
|
+
|
201
|
+
app.post '/forgot' do
|
202
|
+
redirect '/home' if authenticated?
|
203
|
+
redirect '/' unless params['user']
|
204
|
+
|
205
|
+
user = User.first_by_login(params['user']['login'])
|
206
|
+
|
207
|
+
if user.nil?
|
208
|
+
notify :error, :forgot_no_user
|
209
|
+
redirect back
|
210
|
+
end
|
211
|
+
|
212
|
+
user.forgot_password!
|
213
|
+
Pony.mail(
|
214
|
+
:to => user.email,
|
215
|
+
:from => "no-reply@#{env['SERVER_NAME']}",
|
216
|
+
:body => token_link('reset', user))
|
217
|
+
notify :success, :forgot_success
|
218
|
+
redirect '/login'
|
219
|
+
end
|
220
|
+
|
221
|
+
app.get '/reset/:token/?' do
|
222
|
+
redirect '/home' if authenticated?
|
223
|
+
|
224
|
+
if params[:token].nil? || params[:token].empty?
|
225
|
+
notify :error, :reset_no_token
|
226
|
+
redirect '/'
|
227
|
+
end
|
228
|
+
|
229
|
+
user = User.first(:confirm_token => params[:token])
|
230
|
+
if user.nil?
|
231
|
+
notify :error, :reset_no_user
|
232
|
+
redirect '/login'
|
233
|
+
end
|
234
|
+
|
235
|
+
haml :reset, :locals => { :confirm_token => user.confirm_token }
|
236
|
+
end
|
237
|
+
|
238
|
+
app.post '/reset' do
|
239
|
+
redirect '/home' if authenticated?
|
240
|
+
redirect '/' unless params['user']
|
241
|
+
|
242
|
+
user = User.first(:confirm_token => params[:user][:confirm_token])
|
243
|
+
if user.nil?
|
244
|
+
notify :error, :reset_no_user
|
245
|
+
redirect '/login'
|
246
|
+
end
|
247
|
+
|
248
|
+
success = user.reset_password!(
|
249
|
+
params['user']['password'],
|
250
|
+
params['user']['password_confirmation'])
|
251
|
+
|
252
|
+
unless success
|
253
|
+
notify :error, :reset_unmatched_passwords
|
254
|
+
redirect back
|
255
|
+
end
|
256
|
+
|
257
|
+
user.confirm_email!
|
258
|
+
env['warden'].set_user(user)
|
259
|
+
notify :success, :reset_success
|
260
|
+
redirect '/home'
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
class Middleware < Sinatra::Base
|
266
|
+
register Base
|
267
|
+
register RememberMe
|
268
|
+
register ForgotPassword
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Doorman
|
3
|
+
Messages = {
|
4
|
+
:auth_required => 'You must be logged in to view this page.',
|
5
|
+
:signup_success => 'You have signed up successfully. A confirmation ' +
|
6
|
+
'email has been sent to you.',
|
7
|
+
:confirm_no_token => 'Invalid confirmation URL. Please make sure you ' +
|
8
|
+
'have the correct link from the email.',
|
9
|
+
:confirm_no_user => 'Invalid confirmation URL. Please make sure you ' +
|
10
|
+
'have the correct link from the email, and are not already confirmed.',
|
11
|
+
:confirm_success => 'You have successfully confirmed your account. ' +
|
12
|
+
'Please log in.',
|
13
|
+
# Auto login upon confirmation?
|
14
|
+
:login_bad_credentials => 'Invalid Login and Password. Please try again.',
|
15
|
+
:login_not_confirmed => 'You must confirm your account before you can ' +
|
16
|
+
'log in. Please click the confirmation link sent to you.',
|
17
|
+
# Note: resend confirmation link?
|
18
|
+
:logout_success => 'You have been logged out.',
|
19
|
+
:forgot_no_user => 'There is no user with that Username or Email. ' +
|
20
|
+
'Please try again.',
|
21
|
+
:forgot_success => 'An email with instructions to reset your password ' +
|
22
|
+
'has been sent to you.',
|
23
|
+
:reset_no_token => 'Invalid reset URL. Please make sure you ' +
|
24
|
+
'have the correct link from the email.',
|
25
|
+
:reset_no_user => 'Invalid reset URL. Please make sure you have the ' +
|
26
|
+
'correct link from the email, and have already reset the password.',
|
27
|
+
:reset_unmatched_passwords => 'Password and confirmation do not match. ' +
|
28
|
+
'Please try again.',
|
29
|
+
:reset_success => 'Your password has been reset.'
|
30
|
+
}
|
31
|
+
end
|
32
|
+
end
|
data/lib/doorman/user.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'dm-core'
|
2
|
+
require 'dm-validations'
|
3
|
+
require 'dm-timestamps'
|
4
|
+
|
5
|
+
module Sinatra
|
6
|
+
module Doorman
|
7
|
+
class User
|
8
|
+
include DataMapper::Resource
|
9
|
+
|
10
|
+
property :id, Serial
|
11
|
+
property :username, String, :unique => true, :length => 1..23, :format => /^[a-zA-Z0-9\-_]*$/
|
12
|
+
property :email, String, :unique => true, :required => true, :format => :email_address
|
13
|
+
property :password_hash, String, :accessor => :protected
|
14
|
+
property :salt, String, :accessor => :protected
|
15
|
+
|
16
|
+
property :confirmed, Boolean, :writer => :protected
|
17
|
+
property :confirm_token, String, :writer => :protected
|
18
|
+
property :remember_token, String, :writer => :protected
|
19
|
+
|
20
|
+
property :created_at, DateTime
|
21
|
+
property :last_login, DateTime
|
22
|
+
|
23
|
+
attr_accessor :password, :password_confirmation
|
24
|
+
|
25
|
+
validates_length :password, :min => 4, :if => :new?
|
26
|
+
validates_is_confirmed :password
|
27
|
+
|
28
|
+
before :create do
|
29
|
+
if valid?
|
30
|
+
self.password_hash = encrypt(password)
|
31
|
+
self.confirm_token = new_token
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.first_by_login(login)
|
36
|
+
#if login has @ symbol, treat as email address
|
37
|
+
column = ( login =~ /@/ ? :email : :username )
|
38
|
+
User.first(column => login)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.authenticate(login, password)
|
42
|
+
user = User.first_by_login(login)
|
43
|
+
return user if user && user.authenticated?(password)
|
44
|
+
return nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def authenticated?(password)
|
48
|
+
self.password_hash == encrypt(password)
|
49
|
+
end
|
50
|
+
|
51
|
+
def remember_me!
|
52
|
+
self.remember_token = new_token
|
53
|
+
save
|
54
|
+
end
|
55
|
+
|
56
|
+
def forget_me!
|
57
|
+
self.remember_token = nil
|
58
|
+
save
|
59
|
+
end
|
60
|
+
|
61
|
+
def confirm_email!
|
62
|
+
self.confirmed = true
|
63
|
+
self.confirm_token = nil
|
64
|
+
save
|
65
|
+
end
|
66
|
+
|
67
|
+
def forgot_password!
|
68
|
+
self.confirm_token = new_token
|
69
|
+
save
|
70
|
+
end
|
71
|
+
|
72
|
+
def remembered_password!
|
73
|
+
self.confirm_token = nil
|
74
|
+
save
|
75
|
+
end
|
76
|
+
|
77
|
+
def reset_password!(new_password, new_password_confirmation)
|
78
|
+
self.password = new_password
|
79
|
+
self.password_confirmation = new_password_confirmation
|
80
|
+
self.password_hash = encrypt(password) if valid?
|
81
|
+
save
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def salt
|
87
|
+
if @salt.nil? || @salt.empty?
|
88
|
+
secret = Digest::SHA1.hexdigest("--#{Time.now.utc}--")
|
89
|
+
self.salt = Digest::SHA1.hexdigest("--#{Time.now.utc}--#{secret}--")
|
90
|
+
end
|
91
|
+
@salt
|
92
|
+
end
|
93
|
+
|
94
|
+
def encrypt(string)
|
95
|
+
Digest::SHA1.hexdigest("--#{salt}--#{string}--")
|
96
|
+
end
|
97
|
+
|
98
|
+
def new_token
|
99
|
+
encrypt("--#{Time.now.utc}--")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rack
|
2
|
+
class Cookies
|
3
|
+
class CookieJar < Hash
|
4
|
+
def initialize(cookies)
|
5
|
+
@set_cookies = {}
|
6
|
+
@delete_cookies = {}
|
7
|
+
super()
|
8
|
+
update(cookies)
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
super(name.to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def []=(key, options)
|
16
|
+
unless options.is_a?(Hash)
|
17
|
+
options = { :value => options }
|
18
|
+
end
|
19
|
+
|
20
|
+
options[:path] ||= '/'
|
21
|
+
@set_cookies[key] = options
|
22
|
+
super(key.to_s, options[:value])
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete(key, options = {})
|
26
|
+
options[:path] ||= '/'
|
27
|
+
@delete_cookies[key] = options
|
28
|
+
super(key.to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
def finish!(resp)
|
32
|
+
@set_cookies.each { |k, v| resp.set_cookie(k, v) }
|
33
|
+
@delete_cookies.each { |k, v| resp.delete_cookie(k, v) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def initialize(app)
|
38
|
+
@app = app
|
39
|
+
end
|
40
|
+
|
41
|
+
def call(env)
|
42
|
+
req = Request.new(env)
|
43
|
+
env['rack.cookies'] = cookies = CookieJar.new(req.cookies)
|
44
|
+
status, headers, body = @app.call(env)
|
45
|
+
resp = Response.new(body, status, headers)
|
46
|
+
cookies.finish!(resp)
|
47
|
+
resp.to_a
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/views/forgot.haml
ADDED
data/views/login.haml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
%form{:action => "/login", :id => "login", :method => "POST"}
|
2
|
+
%label{:for => "user[login]"} Username or Email
|
3
|
+
%input{:id => "user[login]", :name => "user[login]", :type => "text"}
|
4
|
+
%label{:for => "user[password]"} Password
|
5
|
+
%input{:id => "user[password]", :name => "user[password]", :type => "text"}
|
6
|
+
%label{:for => "user[remember_me]"} Remember Me
|
7
|
+
%input{:id => "user[remember_me]", :name => "user[remember_me]", :type => "checkbox"}
|
8
|
+
%input{:id => "login", :name => "login", :type => "submit", :value => "Login"}
|
9
|
+
%p link to signup
|
data/views/reset.haml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
%form{:action => "/reset", :id => "reset", :method => "POST"}
|
2
|
+
%label{:for => "user[password]"} Password
|
3
|
+
%input{:id => "user[password]", :name => "user[password]", :type => "text"}
|
4
|
+
%label{:for => "user[password_confirmation]"} Confirm Password
|
5
|
+
%input{:id => "user[password_confirmation]", :name => "user[password_confirmation]", :type => "text"}
|
6
|
+
%input{:id => "user[confirm_token]", :name => "user[confirm_token]", :type => "hidden", :value => confirm_token}
|
7
|
+
%input{:id => "reset", :name => "reset", :type => "submit", :value => "Reset Password"}
|
data/views/signup.haml
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
%form{:action => "/signup", :id => "signup", :method => "POST"}
|
2
|
+
%label{:for => "user[username]"} Username
|
3
|
+
%input{:id => "user[username]", :name => "user[username]", :type => "text"}
|
4
|
+
%label{:for => "user[email]"} Email Address
|
5
|
+
%input{:id => "user[email]", :name => "user[email]", :type => "text"}
|
6
|
+
%label{:for => "user[password]"} Password
|
7
|
+
%input{:id => "user[password]", :name => "user[password]", :type => "text"}
|
8
|
+
%label{:for => "user[password_confirmation]"} Confirm Password
|
9
|
+
%input{:id => "user[password_confirmation]", :name => "user[password_confirmation]", :type => "text"}
|
10
|
+
%input{:id => "signup", :name => "signup", :type => "submit", :value => "Sign up!"}
|
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sinatra-doorman
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Mendonca
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-02-10 00:00:00 -08: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: 1.0.a
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: warden
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.9.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: pony
|
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-core
|
47
|
+
type: :runtime
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.10.2
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: dm-validations
|
57
|
+
type: :runtime
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ~>
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 0.10.2
|
64
|
+
version:
|
65
|
+
- !ruby/object:Gem::Dependency
|
66
|
+
name: dm-timestamps
|
67
|
+
type: :runtime
|
68
|
+
version_requirement:
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ~>
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 0.10.2
|
74
|
+
version:
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rspec
|
77
|
+
type: :development
|
78
|
+
version_requirement:
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: "0"
|
84
|
+
version:
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: cucumber
|
87
|
+
type: :development
|
88
|
+
version_requirement:
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: "0"
|
94
|
+
version:
|
95
|
+
- !ruby/object:Gem::Dependency
|
96
|
+
name: webrat
|
97
|
+
type: :development
|
98
|
+
version_requirement:
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: "0"
|
104
|
+
version:
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
name: rack-test
|
107
|
+
type: :development
|
108
|
+
version_requirement:
|
109
|
+
version_requirements: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: "0"
|
114
|
+
version:
|
115
|
+
description:
|
116
|
+
email: joaosinho@gmail.com
|
117
|
+
executables: []
|
118
|
+
|
119
|
+
extensions: []
|
120
|
+
|
121
|
+
extra_rdoc_files:
|
122
|
+
- README.rdoc
|
123
|
+
files:
|
124
|
+
- MIT-LICENSE
|
125
|
+
- Rakefile
|
126
|
+
- README.rdoc
|
127
|
+
- views/signup.haml
|
128
|
+
- views/reset.haml
|
129
|
+
- views/forgot.haml
|
130
|
+
- views/login.haml
|
131
|
+
- lib/rack/contrib/cookies.rb
|
132
|
+
- lib/doorman.rb
|
133
|
+
- lib/doorman/messages.rb
|
134
|
+
- lib/doorman/user.rb
|
135
|
+
- lib/doorman/base.rb
|
136
|
+
has_rdoc: true
|
137
|
+
homepage: http://github.com/johnmendonca/sinatra-doorman
|
138
|
+
licenses: []
|
139
|
+
|
140
|
+
post_install_message:
|
141
|
+
rdoc_options:
|
142
|
+
- --main
|
143
|
+
- README.rdoc
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
requirements:
|
148
|
+
- - ">="
|
149
|
+
- !ruby/object:Gem::Version
|
150
|
+
version: "0"
|
151
|
+
version:
|
152
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: "0"
|
157
|
+
version:
|
158
|
+
requirements: []
|
159
|
+
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 1.3.5
|
162
|
+
signing_key:
|
163
|
+
specification_version: 3
|
164
|
+
summary: A user authentication middleware built with Sinatra and Warden
|
165
|
+
test_files: []
|
166
|
+
|