fingerrails 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/README.rdoc +3 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/bin/fingerrails +9 -0
- data/fingerrails.gemspec +90 -0
- data/fingertips.rb +181 -0
- data/templates/.kick +22 -0
- data/templates/Rakefile +23 -0
- data/templates/app/controllers/application_controller.rb +51 -0
- data/templates/app/controllers/members_controller.rb +32 -0
- data/templates/app/controllers/passwords_controller.rb +46 -0
- data/templates/app/controllers/sessions_controller.rb +26 -0
- data/templates/app/helpers/application_helper.rb +20 -0
- data/templates/app/models/mailer.rb +8 -0
- data/templates/app/models/member.rb +10 -0
- data/templates/app/models/member/authentication.rb +44 -0
- data/templates/app/views/layouts/_application_javascript_includes.html.erb +3 -0
- data/templates/app/views/layouts/_head.html.erb +3 -0
- data/templates/app/views/layouts/application.html.erb +25 -0
- data/templates/app/views/mailer/reset_password_message.erb +8 -0
- data/templates/app/views/members/edit.html.erb +21 -0
- data/templates/app/views/members/new.html.erb +26 -0
- data/templates/app/views/members/show.html.erb +3 -0
- data/templates/app/views/passwords/edit.html.erb +22 -0
- data/templates/app/views/passwords/new.html.erb +22 -0
- data/templates/app/views/passwords/reset.html.erb +9 -0
- data/templates/app/views/passwords/sent.html.erb +11 -0
- data/templates/app/views/sessions/_form.html.erb +22 -0
- data/templates/app/views/sessions/_status.html.erb +9 -0
- data/templates/app/views/sessions/new.html.erb +5 -0
- data/templates/config/database.yml +18 -0
- data/templates/lib/active_record_ext.rb +26 -0
- data/templates/lib/token.rb +9 -0
- data/templates/public/403.html +29 -0
- data/templates/public/javascripts/ready.js +9 -0
- data/templates/public/stylesheets/default.css +143 -0
- data/templates/public/stylesheets/reset.css +52 -0
- data/templates/test/ext/authentication.rb +24 -0
- data/templates/test/ext/file_fixtures.rb +8 -0
- data/templates/test/ext/time.rb +8 -0
- data/templates/test/fixtures/members.yml +8 -0
- data/templates/test/functional/application_controller_test.rb +104 -0
- data/templates/test/functional/members_controller_test.rb +71 -0
- data/templates/test/functional/passwords_controller_test.rb +95 -0
- data/templates/test/functional/sessions_controller_test.rb +68 -0
- data/templates/test/lib/active_record_ext_test.rb +13 -0
- data/templates/test/lib/token_test.rb +17 -0
- data/templates/test/test_helper.rb +32 -0
- data/templates/test/unit/helpers/application_helper_test.rb +44 -0
- data/templates/test/unit/mailer_test.rb +13 -0
- data/templates/test/unit/member/authentication_test.rb +73 -0
- data/templates/test/unit/member_test.rb +32 -0
- metadata +108 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
|
3
|
+
class PasswordsController < ApplicationController
|
4
|
+
allow_access :all
|
5
|
+
|
6
|
+
prepend_before_filter :find_member_by_token, :only => [:edit, :update]
|
7
|
+
|
8
|
+
def create
|
9
|
+
if @member = Member.find_by_email(params[:email])
|
10
|
+
@member.generate_reset_password_token!
|
11
|
+
Mailer.deliver_reset_password_message(@member, edit_password_url(:id => @member.reset_password_token))
|
12
|
+
render :sent
|
13
|
+
else
|
14
|
+
flash[:error] = "We couldn’t find an account with the email address you entered. Please try again."
|
15
|
+
render :new
|
16
|
+
end
|
17
|
+
rescue Net::SMTPError => e
|
18
|
+
smtp_error(e)
|
19
|
+
render :new
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
@member.password = params[:password]
|
24
|
+
if @member.save
|
25
|
+
render :reset
|
26
|
+
else
|
27
|
+
render :edit
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def find_member_by_token
|
34
|
+
unless @member = Member.find_by_reset_password_token(params[:id])
|
35
|
+
send_response_document :not_found
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def smtp_error(e)
|
40
|
+
logger.error "#{e.class} raised while trying to email: #{e.message}
|
41
|
+
|
42
|
+
#{e.backtrace.join("
|
43
|
+
")}"
|
44
|
+
flash[:error] = "We're sorry, but your email could not be sent. Please try again later."
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class SessionsController < ApplicationController
|
2
|
+
allow_access :all
|
3
|
+
|
4
|
+
def new
|
5
|
+
still_authentication_needed!
|
6
|
+
@unauthenticated = Member.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def create
|
10
|
+
@unauthenticated = Member.authenticate(params[:member])
|
11
|
+
if @unauthenticated.errors.blank?
|
12
|
+
login(@unauthenticated)
|
13
|
+
finish_authentication_needed! || redirect_to(root_url)
|
14
|
+
else
|
15
|
+
still_authentication_needed!
|
16
|
+
flash[:login_error] = @unauthenticated.errors.on(:base)
|
17
|
+
render :new
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear
|
22
|
+
logout
|
23
|
+
flash[:notice] = "You are now logged out."
|
24
|
+
redirect_to root_url
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApplicationHelper
|
2
|
+
def nav_link_to(label, url, options={})
|
3
|
+
if current_page?(url)
|
4
|
+
options[:class] ? options[:class] << ' current' : options[:class] = 'current'
|
5
|
+
end
|
6
|
+
link_to(label, url, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def nav_item(label, url, options={})
|
10
|
+
shallow = options.delete(:shallow)
|
11
|
+
|
12
|
+
classes = (options[:class] || '').split(' ')
|
13
|
+
if (shallow and request.request_uri == url) or (!shallow and request.request_uri.start_with?(url))
|
14
|
+
classes << 'current'
|
15
|
+
end
|
16
|
+
options[:class] = classes.empty? ? nil : classes.join(' ')
|
17
|
+
|
18
|
+
content_tag(:li, link_to(label, url), options)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'digest/sha1'
|
2
|
+
|
3
|
+
class Member
|
4
|
+
attr_accessible :password, :verify_password
|
5
|
+
|
6
|
+
def generate_reset_password_token!
|
7
|
+
update_attribute :reset_password_token, Token.generate
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :password
|
11
|
+
def password=(password)
|
12
|
+
self.hashed_password = self.class.hash_password(password)
|
13
|
+
end
|
14
|
+
|
15
|
+
def verify_password=(password)
|
16
|
+
@verify_password = self.class.hash_password(password)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.hash_password(password)
|
20
|
+
::Digest::SHA1.hexdigest(password)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Authenticates credentials. Takes a hash with a :email and :password, returns an instance of Member.
|
24
|
+
# The Member has errors on base when the user isn't authenticated.
|
25
|
+
def self.authenticate(params={})
|
26
|
+
unless member = find_by_email_and_hashed_password(params[:email], hash_password(params[:password]))
|
27
|
+
member = Member.new
|
28
|
+
member.errors.add_to_base("The username and/or email you entered is invalid. Please try again.")
|
29
|
+
member
|
30
|
+
else
|
31
|
+
member
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def password_is_not_blank
|
38
|
+
if hashed_password == self.class.hash_password('')
|
39
|
+
errors.add(:password, "can't be blank")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
validate :password_is_not_blank
|
44
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<%= render :partial => 'layouts/head' %>
|
5
|
+
</head>
|
6
|
+
<body>
|
7
|
+
<div id="wrapper">
|
8
|
+
<div id="header">
|
9
|
+
<a id="logo" href="/">{{AppName}}</a>
|
10
|
+
<%= render :partial => 'sessions/status' %>
|
11
|
+
|
12
|
+
<ul id="navigation">
|
13
|
+
<%= nav_item 'Home', root_path, :class => 'first', :shallow => true %>
|
14
|
+
<%= nav_item 'Members', members_path, :class => 'last' %>
|
15
|
+
</ul>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div id="main">
|
19
|
+
<%= yield :layout %>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<%= render :partial => 'layouts/application_javascript_includes' %>
|
24
|
+
</body>
|
25
|
+
</html>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<% @title = 'Edit profile' %>
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<% form_for @member do |f| %>
|
5
|
+
<h2>Edit profile</h2>
|
6
|
+
|
7
|
+
<%= error_messages_for :member %>
|
8
|
+
|
9
|
+
<div class="fields">
|
10
|
+
<div class="field">
|
11
|
+
<div class="label"><%= f.label :email %></div>
|
12
|
+
<div class="field"><%= f.text_field :email %></div>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div class="submit">
|
16
|
+
<%= f.submit 'Update profile' %>
|
17
|
+
<%= link_to 'Back', root_path, :class => 'cancel' %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
<% end %>
|
21
|
+
</div>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% @title = 'Sign up' %>
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<% form_for @member do |f| %>
|
5
|
+
<h2>Sign up</h2>
|
6
|
+
|
7
|
+
<%= error_messages_for :member %>
|
8
|
+
|
9
|
+
<div class="fields">
|
10
|
+
<div class="field">
|
11
|
+
<div class="label"><%= f.label :email %></div>
|
12
|
+
<div class="field"><%= f.text_field :email %></div>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div class="field">
|
16
|
+
<div class="label"><%= f.label :password %></div>
|
17
|
+
<div class="field"><%= f.password_field :password %></div>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<div class="submit">
|
21
|
+
<%= f.submit 'Sign up' %>
|
22
|
+
<%= link_to 'Back', root_path, :class => 'cancel' %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
</div>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% @title = 'Choose a new password' %>
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<% form_tag password_path(:id => @member.reset_password_token), :method => :put do %>
|
5
|
+
<h2><%= @title = 'Choose a new password' %></h2>
|
6
|
+
<p>You’ll be able to log in after you’ve chosen a new password.</p>
|
7
|
+
|
8
|
+
<% if @member.errors.on(:password) %>
|
9
|
+
<div class="errorExplanation">The password can’t be blank.</div>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<div>
|
13
|
+
<div class="label"><%= label_tag :password, 'New password' %></div>
|
14
|
+
<div class="field"><%= password_field_tag :password %></div>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div>
|
18
|
+
<%= submit_tag 'Continue' %>
|
19
|
+
<%= link_to 'Cancel', root_path, :class => 'cancel' %>
|
20
|
+
</div>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% @title = 'Choose a new password' %>
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<% form_tag passwords_path do %>
|
5
|
+
<h2>Forgot password?</h2>
|
6
|
+
<p>Please enter your email address and we’ll send further instructions on how to choose a new password.</p>
|
7
|
+
|
8
|
+
<% unless flash[:error].blank? %>
|
9
|
+
<div class="errorExplanation"><%= flash[:error] %></div>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<div class="field">
|
13
|
+
<div class="label"><%= label_tag :email, 'Email address' %></div>
|
14
|
+
<div class="field"><%= text_field_tag :email %></div>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div class="submit">
|
18
|
+
<%= submit_tag 'Continue' %>
|
19
|
+
<%= link_to 'Cancel', root_path, :class => 'cancel' %>
|
20
|
+
</div>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<% @title = 'Choose a new password' %>
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<% form_tag root_path, :method => :get do %>
|
5
|
+
<h2>Forgot password?</h2>
|
6
|
+
<p>We’ve sent further instructions on how to choose a new password by email.</p>
|
7
|
+
<div>
|
8
|
+
<%= submit_tag 'Okay' %>
|
9
|
+
</div>
|
10
|
+
<% end %>
|
11
|
+
</div>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<% form_for(@unauthenticated || Member.new, :url => session_path) do |f| %>
|
2
|
+
<h2><%=h @title = 'Log in' %></h2>
|
3
|
+
|
4
|
+
<% if flash[:login_error] %>
|
5
|
+
<div class="errorExplanation"><%= flash[:login_error] %></div>
|
6
|
+
<% end %>
|
7
|
+
|
8
|
+
<div class="field">
|
9
|
+
<div class="label"><%= f.label :email %></div>
|
10
|
+
<div class="field"><%= f.text_field :email, :tabindex => 1 %></div>
|
11
|
+
</div>
|
12
|
+
|
13
|
+
<div class="field">
|
14
|
+
<div class="label"><%= f.label :password %> <%= link_to 'forgot password?', new_password_path, :tabindex => 5 %></div>
|
15
|
+
<div class="field"><%= f.password_field :password, :tabindex => 2 %></div>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="field">
|
19
|
+
<%= f.submit 'Log in', :tabindex => 4 %>
|
20
|
+
<%= link_to 'Cancel', root_path, :class => 'cancel', :tabindex => 5 %>
|
21
|
+
</div>
|
22
|
+
<% end %>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<p id="member">
|
2
|
+
<% if @authenticated %>
|
3
|
+
<%= nav_link_to 'Profile', @authenticated %>
|
4
|
+
<%= link_to 'Log out', clear_session_path %>
|
5
|
+
<% else %>
|
6
|
+
<%= nav_link_to 'Log in', new_session_path, 'class' => 'login' %>
|
7
|
+
<%= nav_link_to 'Sign up', new_member_path %>
|
8
|
+
<% end %>
|
9
|
+
</p>
|
@@ -0,0 +1,18 @@
|
|
1
|
+
development:
|
2
|
+
adapter: mysql
|
3
|
+
encoding: utf8
|
4
|
+
reconnect: false
|
5
|
+
database: {{app_name}}_development
|
6
|
+
pool: 5
|
7
|
+
test:
|
8
|
+
adapter: mysql
|
9
|
+
encoding: utf8
|
10
|
+
reconnect: false
|
11
|
+
database: {{app_name}}_test
|
12
|
+
pool: 5
|
13
|
+
production:
|
14
|
+
adapter: mysql
|
15
|
+
encoding: utf8
|
16
|
+
reconnect: false
|
17
|
+
database: {{app_name}}_production
|
18
|
+
pool: 5
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Ext
|
3
|
+
# Loads various parts of a class definition, a simple way to separate large classes.
|
4
|
+
#
|
5
|
+
# class Member
|
6
|
+
# embrace :authentication
|
7
|
+
# end
|
8
|
+
def embrace(*parts)
|
9
|
+
parts.each do |part|
|
10
|
+
require_dependency "#{name.downcase}/#{part}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module BasicScopes
|
16
|
+
def self.included(base)
|
17
|
+
base.named_scope(:order, Proc.new do |attribute, direction|
|
18
|
+
order = "#{attribute}"
|
19
|
+
order << " #{direction.to_s.upcase}" unless direction.blank?
|
20
|
+
{ :order => order }
|
21
|
+
end)
|
22
|
+
|
23
|
+
base.named_scope :limit, Proc.new { |limit| { :limit => limit } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Token
|
2
|
+
DEFAULT_LENGTH = 8
|
3
|
+
|
4
|
+
def self.generate(requested_length=DEFAULT_LENGTH)
|
5
|
+
length = requested_length.odd? ? requested_length + 1 : requested_length
|
6
|
+
token = (1..length/2).map { |i| (1..2).map { (i.odd? ? ('a'..'z') : ('0'..'9')).to_a.rand }.join }.join
|
7
|
+
token[0...requested_length]
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
2
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
3
|
+
|
4
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
5
|
+
|
6
|
+
<head>
|
7
|
+
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
8
|
+
<title>Access forbidden (403)</title>
|
9
|
+
<style type="text/css">
|
10
|
+
body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
|
11
|
+
div.dialog {
|
12
|
+
width: 25em;
|
13
|
+
padding: 0 4em;
|
14
|
+
margin: 4em auto 0 auto;
|
15
|
+
border: 1px solid #ccc;
|
16
|
+
border-right-color: #999;
|
17
|
+
border-bottom-color: #999;
|
18
|
+
}
|
19
|
+
h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
|
20
|
+
</style>
|
21
|
+
</head>
|
22
|
+
|
23
|
+
<body>
|
24
|
+
<!-- This file lives in public/403.html -->
|
25
|
+
<div class="dialog">
|
26
|
+
<h1>Access forbidden.</h1>
|
27
|
+
</div>
|
28
|
+
</body>
|
29
|
+
</html>
|