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