fingerrails 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/.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>
|