sinatra-doorman 0.1.0
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/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
|
+
|