fingerrails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/.gitignore +3 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +28 -0
  4. data/VERSION +1 -0
  5. data/bin/fingerrails +9 -0
  6. data/fingerrails.gemspec +90 -0
  7. data/fingertips.rb +181 -0
  8. data/templates/.kick +22 -0
  9. data/templates/Rakefile +23 -0
  10. data/templates/app/controllers/application_controller.rb +51 -0
  11. data/templates/app/controllers/members_controller.rb +32 -0
  12. data/templates/app/controllers/passwords_controller.rb +46 -0
  13. data/templates/app/controllers/sessions_controller.rb +26 -0
  14. data/templates/app/helpers/application_helper.rb +20 -0
  15. data/templates/app/models/mailer.rb +8 -0
  16. data/templates/app/models/member.rb +10 -0
  17. data/templates/app/models/member/authentication.rb +44 -0
  18. data/templates/app/views/layouts/_application_javascript_includes.html.erb +3 -0
  19. data/templates/app/views/layouts/_head.html.erb +3 -0
  20. data/templates/app/views/layouts/application.html.erb +25 -0
  21. data/templates/app/views/mailer/reset_password_message.erb +8 -0
  22. data/templates/app/views/members/edit.html.erb +21 -0
  23. data/templates/app/views/members/new.html.erb +26 -0
  24. data/templates/app/views/members/show.html.erb +3 -0
  25. data/templates/app/views/passwords/edit.html.erb +22 -0
  26. data/templates/app/views/passwords/new.html.erb +22 -0
  27. data/templates/app/views/passwords/reset.html.erb +9 -0
  28. data/templates/app/views/passwords/sent.html.erb +11 -0
  29. data/templates/app/views/sessions/_form.html.erb +22 -0
  30. data/templates/app/views/sessions/_status.html.erb +9 -0
  31. data/templates/app/views/sessions/new.html.erb +5 -0
  32. data/templates/config/database.yml +18 -0
  33. data/templates/lib/active_record_ext.rb +26 -0
  34. data/templates/lib/token.rb +9 -0
  35. data/templates/public/403.html +29 -0
  36. data/templates/public/javascripts/ready.js +9 -0
  37. data/templates/public/stylesheets/default.css +143 -0
  38. data/templates/public/stylesheets/reset.css +52 -0
  39. data/templates/test/ext/authentication.rb +24 -0
  40. data/templates/test/ext/file_fixtures.rb +8 -0
  41. data/templates/test/ext/time.rb +8 -0
  42. data/templates/test/fixtures/members.yml +8 -0
  43. data/templates/test/functional/application_controller_test.rb +104 -0
  44. data/templates/test/functional/members_controller_test.rb +71 -0
  45. data/templates/test/functional/passwords_controller_test.rb +95 -0
  46. data/templates/test/functional/sessions_controller_test.rb +68 -0
  47. data/templates/test/lib/active_record_ext_test.rb +13 -0
  48. data/templates/test/lib/token_test.rb +17 -0
  49. data/templates/test/test_helper.rb +32 -0
  50. data/templates/test/unit/helpers/application_helper_test.rb +44 -0
  51. data/templates/test/unit/mailer_test.rb +13 -0
  52. data/templates/test/unit/member/authentication_test.rb +73 -0
  53. data/templates/test/unit/member_test.rb +32 -0
  54. 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,8 @@
1
+ class Mailer < ActionMailer::Base
2
+ def reset_password_message(member, url)
3
+ recipients member.email
4
+ from SYSTEM_EMAIL_ADDRESS
5
+ subject "[{{AppName}}] Confirm password reset"
6
+ body :member => member, :url => url
7
+ end
8
+ end
@@ -0,0 +1,10 @@
1
+ class Member < ActiveRecord::Base
2
+ embrace :authentication
3
+
4
+ attr_accessible :email
5
+
6
+ private
7
+
8
+ validates_uniqueness_of :email
9
+ validates_email :email
10
+ 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,3 @@
1
+ <%#= javascript_include_tag(%w(prototype effects controls) +
2
+ %w() + # add app specific libs
3
+ %w(ready), :cache => (Rails.env == 'production')) %>
@@ -0,0 +1,3 @@
1
+ <meta charset="UTF-8">
2
+ <title><%=h [@title, '{{AppName}}'].compact.join(' · ') %></title>
3
+ <%= stylesheet_link_tag %w(reset default), :media => 'all', :cache => 'application' %>
@@ -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,8 @@
1
+ Hi,
2
+
3
+ Forgot your password? Click on the link below to set a new password.
4
+
5
+ <%= @url %>
6
+
7
+ Kind regards,
8
+ {{AppName}}
@@ -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,3 @@
1
+ <% @title = @member.email %>
2
+
3
+ Member show: <%= @member.email %>
@@ -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,9 @@
1
+ <div>
2
+ <% form_tag new_session_path, :method => :get do %>
3
+ <h2><%=h @title = 'Choose a new password' %></h2>
4
+ <p>Your password has been changed.</p>
5
+ <div>
6
+ <%= submit_tag 'Okay' %>
7
+ </div>
8
+ <% end %>
9
+ </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,5 @@
1
+ <% @title = 'Log in' %>
2
+
3
+ <div>
4
+ <%= render :partial => 'sessions/form' %>
5
+ </div>
@@ -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>