authpwn_rails 0.10.5 → 0.10.6
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/Gemfile +8 -8
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/app/models/credentials/email.rb +12 -4
- data/app/models/credentials/token.rb +106 -0
- data/app/models/tokens/email_verification.rb +42 -0
- data/app/models/tokens/one_time.rb +16 -0
- data/app/models/tokens/password_reset.rb +27 -0
- data/authpwn_rails.gemspec +36 -11
- data/lib/authpwn_rails.rb +2 -0
- data/lib/authpwn_rails/generators/all_generator.rb +20 -2
- data/lib/authpwn_rails/generators/templates/credentials.yml +21 -0
- data/lib/authpwn_rails/generators/templates/session/new.html.erb +10 -5
- data/lib/authpwn_rails/generators/templates/session/password_change.html.erb +37 -0
- data/lib/authpwn_rails/generators/templates/session_controller.rb +20 -2
- data/lib/authpwn_rails/generators/templates/session_controller_test.rb +71 -0
- data/lib/authpwn_rails/generators/templates/session_mailer.rb +26 -0
- data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.html.erb +23 -0
- data/lib/authpwn_rails/generators/templates/session_mailer/email_verification_email.text.erb +11 -0
- data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.html.erb +23 -0
- data/lib/authpwn_rails/generators/templates/session_mailer/reset_password_email.text.erb +11 -0
- data/lib/authpwn_rails/generators/templates/session_mailer_test.rb +37 -0
- data/lib/authpwn_rails/routes.rb +50 -0
- data/lib/authpwn_rails/session_controller.rb +129 -0
- data/lib/authpwn_rails/session_mailer.rb +66 -0
- data/test/{email_credential_test.rb → credentials/email_credential_test.rb} +1 -1
- data/test/credentials/email_verification_token_test.rb +78 -0
- data/test/{facebook_credential_test.rb → credentials/facebook_credential_test.rb} +1 -1
- data/test/credentials/one_time_token_credential_test.rb +84 -0
- data/test/{password_credential_test.rb → credentials/password_credential_test.rb} +1 -1
- data/test/credentials/password_reset_token_test.rb +72 -0
- data/test/credentials/token_crendential_test.rb +102 -0
- data/test/fixtures/bare_session/forbidden.html.erb +20 -0
- data/test/fixtures/bare_session/home.html.erb +5 -0
- data/test/fixtures/bare_session/new.html.erb +32 -0
- data/test/fixtures/bare_session/password_change.html.erb +30 -0
- data/test/fixtures/bare_session/welcome.html.erb +5 -0
- data/test/helpers/action_mailer.rb +8 -0
- data/test/helpers/routes.rb +8 -2
- data/test/routes_test.rb +31 -0
- data/test/session_controller_api_test.rb +310 -15
- data/test/session_mailer_api_test.rb +67 -0
- data/test/test_helper.rb +3 -1
- data/test/{email_field_test.rb → user_extensions/email_field_test.rb} +1 -1
- data/test/{facebook_fields_test.rb → user_extensions/facebook_fields_test.rb} +1 -1
- data/test/{password_field_test.rb → user_extensions/password_field_test.rb} +1 -1
- metadata +49 -24
@@ -0,0 +1,66 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Authpwn
|
3
|
+
|
4
|
+
# Included by the session mailer class.
|
5
|
+
#
|
6
|
+
# Parts of the codebase assume the mailer will be named SessionMailer.
|
7
|
+
module SessionMailer
|
8
|
+
# Creates an e-mail containing a verification token for the e-mail address.
|
9
|
+
#
|
10
|
+
# Params:
|
11
|
+
# token:: the e-mail confirmation token
|
12
|
+
# host:: the server's hostname (e.g. "localhost:3000")
|
13
|
+
def email_verification_email(token, host)
|
14
|
+
@token, @host = token, host
|
15
|
+
|
16
|
+
hostname = host.split(':', 2).first # Strip out any port.
|
17
|
+
|
18
|
+
mail :to => @token.email,
|
19
|
+
:subject => email_verification_subject(token, hostname),
|
20
|
+
:from => email_verification_from(token, hostname)
|
21
|
+
end
|
22
|
+
|
23
|
+
# The subject line in an e-mail verification e-mail.
|
24
|
+
#
|
25
|
+
# The authpwn generator encourages applications to override this method.
|
26
|
+
def email_verification_subject(token, server_hostname)
|
27
|
+
"#{server_hostname} e-mail verification"
|
28
|
+
end
|
29
|
+
|
30
|
+
# The sender e-mail address for an e-mail verification e-mail.
|
31
|
+
#
|
32
|
+
# The authpwn generator encourages applications to override this method.
|
33
|
+
def email_verification_from(token, server_hostname)
|
34
|
+
%Q|"#{server_hostname} staff" <admin@#{server_hostname}>|
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates an e-mail containing a password reset token.
|
38
|
+
#
|
39
|
+
# Params:
|
40
|
+
# email:: the email to send the token to
|
41
|
+
# token:: the password reset token
|
42
|
+
# host:: the server's hostname (e.g. "localhost:3000")
|
43
|
+
def reset_password_email(email, token, host)
|
44
|
+
@email, @token, @host = email, token, host
|
45
|
+
|
46
|
+
hostname = host.split(':', 2).first # Strip out any port.
|
47
|
+
mail :to => email, :subject => reset_password_subject(token, hostname),
|
48
|
+
:from => reset_password_from(token, hostname)
|
49
|
+
end
|
50
|
+
|
51
|
+
# The subject line in a password reset e-mail.
|
52
|
+
#
|
53
|
+
# The authpwn generator encourages applications to override this method.
|
54
|
+
def reset_password_subject(token, server_hostname)
|
55
|
+
"#{server_hostname} password reset"
|
56
|
+
end
|
57
|
+
|
58
|
+
# The sender e-mail address for a password reset e-mail.
|
59
|
+
#
|
60
|
+
# The authpwn generator encourages applications to override this method.
|
61
|
+
def reset_password_from(token, server_hostname)
|
62
|
+
%Q|"#{server_hostname} staff" <admin@#{server_hostname}>|
|
63
|
+
end
|
64
|
+
end # namespace Authpwn::SessionMailer
|
65
|
+
|
66
|
+
end # namespace Authpwn
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class EmailVerificationTokenCredentialTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@credential = Tokens::EmailVerification.new(
|
6
|
+
:code => 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo',
|
7
|
+
:key => 'jane@gmail.com')
|
8
|
+
@credential.user = users(:jane)
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'setup' do
|
12
|
+
assert @credential.valid?
|
13
|
+
end
|
14
|
+
|
15
|
+
test 'code required' do
|
16
|
+
@credential.code = nil
|
17
|
+
assert !@credential.valid?
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'code uniqueness' do
|
21
|
+
@credential.code = credentials(:john_token).code
|
22
|
+
assert !@credential.valid?
|
23
|
+
end
|
24
|
+
|
25
|
+
test 'email required' do
|
26
|
+
@credential.email = nil
|
27
|
+
assert !@credential.valid?
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'user required' do
|
31
|
+
@credential.user = nil
|
32
|
+
assert !@credential.valid?
|
33
|
+
end
|
34
|
+
|
35
|
+
test 'email_credential' do
|
36
|
+
assert_equal credentials(:jane_email), @credential.email_credential
|
37
|
+
assert_equal credentials(:john_email),
|
38
|
+
credentials(:john_email_token).email_credential
|
39
|
+
|
40
|
+
@credential.email = 'bill@gmail.com'
|
41
|
+
assert_nil @credential.email_credential
|
42
|
+
end
|
43
|
+
|
44
|
+
test 'spend verifies the e-mail and destroys the token' do
|
45
|
+
email_credential = credentials(:john_email)
|
46
|
+
assert !email_credential.verified, 'bad setup'
|
47
|
+
credential = credentials(:john_email_token)
|
48
|
+
assert_equal Tokens::EmailVerification, credential.class, 'bad setup'
|
49
|
+
|
50
|
+
assert_difference 'Credential.count', -1 do
|
51
|
+
credential.spend
|
52
|
+
end
|
53
|
+
assert credential.frozen?, 'not destroyed'
|
54
|
+
assert email_credential.reload.verified?, 'e-mail not verified'
|
55
|
+
end
|
56
|
+
|
57
|
+
test 'spend does not verify a random e-mail and still destroys the token' do
|
58
|
+
email_credential = credentials(:john_email)
|
59
|
+
credential = credentials(:john_email_token)
|
60
|
+
credential.email = 'bill@gmail.com'
|
61
|
+
|
62
|
+
assert_difference 'Credential.count', -1 do
|
63
|
+
credential.spend
|
64
|
+
end
|
65
|
+
assert credential.frozen?, 'not destroyed'
|
66
|
+
assert !email_credential.reload.verified?, 'e-mail wrongly verified'
|
67
|
+
end
|
68
|
+
|
69
|
+
test 'random_for' do
|
70
|
+
token = Tokens::EmailVerification.random_for credentials(:jane_email)
|
71
|
+
assert token.valid?, 'valid token'
|
72
|
+
assert_equal users(:jane), token.user
|
73
|
+
assert_equal credentials(:jane_email).email, token.email
|
74
|
+
assert_equal Tokens::EmailVerification, token.class
|
75
|
+
assert !token.new_record?, 'saved token'
|
76
|
+
assert_operator users(:jane).credentials, :include?, token
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class OneTimeTokenCredentialTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@credential = Tokens::OneTime.new(
|
6
|
+
:code => 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo')
|
7
|
+
@credential.user = users(:bill)
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'setup' do
|
11
|
+
assert @credential.valid?
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'code required' do
|
15
|
+
@credential.code = nil
|
16
|
+
assert !@credential.valid?
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'code uniqueness' do
|
20
|
+
@credential.code = credentials(:john_token).code
|
21
|
+
assert !@credential.valid?
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'user presence' do
|
25
|
+
@credential.user = nil
|
26
|
+
assert !@credential.valid?
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'spend destroys the token' do
|
30
|
+
credential = credentials(:john_token)
|
31
|
+
assert_equal Tokens::OneTime, credential.class, 'bad setup'
|
32
|
+
|
33
|
+
assert_difference 'Credential.count', -1 do
|
34
|
+
credential.spend
|
35
|
+
end
|
36
|
+
assert credential.frozen?, 'not destroyed'
|
37
|
+
end
|
38
|
+
|
39
|
+
test 'authenticate spends the token' do
|
40
|
+
john = 'YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A'
|
41
|
+
bogus = 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo'
|
42
|
+
assert_difference 'Credential.count', -1, 'token spent' do
|
43
|
+
assert_equal users(:john), Credentials::Token.authenticate(john)
|
44
|
+
end
|
45
|
+
assert_no_difference 'Credential.count', 'token mistakenly spent' do
|
46
|
+
assert_equal :invalid, Credentials::Token.authenticate(bogus)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
test 'authenticate calls User#auth_bounce_reason' do
|
51
|
+
john = 'YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A'
|
52
|
+
jane = '6TXe1vv7BgOw0BkJ1hzUKO6G08fLk4sVfJ3wPDZHS-c'
|
53
|
+
bogus = 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo'
|
54
|
+
|
55
|
+
with_blocked_credential credentials(:john_token), :reason do
|
56
|
+
assert_no_difference 'Credential.count', 'no token spent' do
|
57
|
+
assert_equal :reason, Credentials::Token.authenticate(john)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
test 'instance authenticate spends the token' do
|
63
|
+
assert_difference 'Credential.count', -1, 'token spent' do
|
64
|
+
assert_equal users(:john), credentials(:john_token).authenticate
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
test 'instance authenticate calls User#auth_bounce_reason' do
|
69
|
+
with_blocked_credential credentials(:john_token), :reason do
|
70
|
+
assert_no_difference 'Credential.count', 'token mistakenly spent' do
|
71
|
+
assert_equal :reason, credentials(:john_token).authenticate
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
test 'random_for' do
|
77
|
+
token = Tokens::OneTime.random_for users(:john)
|
78
|
+
assert token.valid?, 'valid token'
|
79
|
+
assert_equal users(:john), token.user
|
80
|
+
assert_equal Tokens::OneTime, token.class
|
81
|
+
assert !token.new_record?, 'saved token'
|
82
|
+
assert_operator users(:john).credentials, :include?, token
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class PasswordVerificationTokenTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@credential = Tokens::PasswordReset.new(
|
6
|
+
:code => 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo')
|
7
|
+
@credential.user = users(:john)
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'setup' do
|
11
|
+
assert @credential.valid?
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'code required' do
|
15
|
+
@credential.code = nil
|
16
|
+
assert !@credential.valid?
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'code uniqueness' do
|
20
|
+
@credential.code = credentials(:john_token).code
|
21
|
+
assert !@credential.valid?
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'user required' do
|
25
|
+
@credential.user = nil
|
26
|
+
assert !@credential.valid?
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'password_credential' do
|
30
|
+
assert_equal credentials(:john_password), @credential.password_credential
|
31
|
+
assert_equal credentials(:jane_password),
|
32
|
+
credentials(:jane_password_token).password_credential
|
33
|
+
|
34
|
+
@credential.user = users(:bill)
|
35
|
+
assert_nil @credential.password_credential
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'spend blanks out the password and destroys the token' do
|
39
|
+
password_credential = credentials(:jane_password)
|
40
|
+
credential = credentials(:jane_password_token)
|
41
|
+
assert_equal Tokens::PasswordReset, credential.class, 'bad setup'
|
42
|
+
|
43
|
+
assert_difference 'Credential.count', -2 do
|
44
|
+
assert_difference 'Credentials::Password.count', -1 do
|
45
|
+
credential.spend
|
46
|
+
end
|
47
|
+
end
|
48
|
+
assert credential.frozen?, 'not destroyed'
|
49
|
+
assert_nil Credential.where(:id => password_credential.id).first,
|
50
|
+
'password not blanked out'
|
51
|
+
end
|
52
|
+
|
53
|
+
test 'spend works on password-less user and still destroys the token' do
|
54
|
+
password_credential = credentials(:jane_password)
|
55
|
+
password_credential.destroy
|
56
|
+
credential = credentials(:jane_password_token)
|
57
|
+
|
58
|
+
assert_difference 'Credential.count', -1 do
|
59
|
+
credential.spend
|
60
|
+
end
|
61
|
+
assert credential.frozen?, 'not destroyed'
|
62
|
+
end
|
63
|
+
|
64
|
+
test 'random_for' do
|
65
|
+
token = Tokens::PasswordReset.random_for users(:john)
|
66
|
+
assert token.valid?, 'valid token'
|
67
|
+
assert_equal users(:john), token.user
|
68
|
+
assert_equal Tokens::PasswordReset, token.class
|
69
|
+
assert !token.new_record?, 'saved token'
|
70
|
+
assert_operator users(:john).credentials, :include?, token
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require File.expand_path('../../test_helper', __FILE__)
|
2
|
+
|
3
|
+
class TokenCredentialTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
@credential = Credentials::Token.new(
|
6
|
+
:code => 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo')
|
7
|
+
@credential.user = users(:bill)
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'setup' do
|
11
|
+
assert @credential.valid?
|
12
|
+
end
|
13
|
+
|
14
|
+
test 'code required' do
|
15
|
+
@credential.code = nil
|
16
|
+
assert !@credential.valid?
|
17
|
+
end
|
18
|
+
|
19
|
+
test 'code uniqueness' do
|
20
|
+
@credential.code = credentials(:john_token).code
|
21
|
+
assert !@credential.valid?
|
22
|
+
end
|
23
|
+
|
24
|
+
test 'user presence' do
|
25
|
+
@credential.user = nil
|
26
|
+
assert !@credential.valid?
|
27
|
+
end
|
28
|
+
|
29
|
+
test 'spend does nothing' do
|
30
|
+
credential = credentials(:jane_token)
|
31
|
+
assert_equal Credentials::Token, credential.class, 'bad setup'
|
32
|
+
|
33
|
+
assert_no_difference 'Credential.count' do
|
34
|
+
credential.spend
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
test 'random_for' do
|
39
|
+
token = Credentials::Token.random_for users(:john)
|
40
|
+
assert token.valid?, 'valid token'
|
41
|
+
assert_equal users(:john), token.user
|
42
|
+
assert_equal Credentials::Token, token.class
|
43
|
+
assert !token.new_record?, 'saved token'
|
44
|
+
assert_operator users(:john).credentials, :include?, token
|
45
|
+
end
|
46
|
+
|
47
|
+
test 'with_code' do
|
48
|
+
john = 'YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A'
|
49
|
+
john2 = 'bDSU4tzfjuob79e3R0ykLcOGTBBYvuBWWJ9V06tQrCE'
|
50
|
+
jane = '6TXe1vv7BgOw0BkJ1hzUKO6G08fLk4sVfJ3wPDZHS-c'
|
51
|
+
bogus = 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo'
|
52
|
+
assert_equal credentials(:john_token), Credentials::Token.with_code(john)
|
53
|
+
assert_equal credentials(:jane_token), Credentials::Token.with_code(jane)
|
54
|
+
assert_equal credentials(:john_email_token),
|
55
|
+
Credentials::Token.with_code(john2)
|
56
|
+
assert_nil Credentials::Token.with_code(bogus)
|
57
|
+
assert_nil Credentials::Token.with_code('john@gmail.com')
|
58
|
+
assert_nil Credentials::Token.with_code(credentials(:jane_email).name)
|
59
|
+
end
|
60
|
+
|
61
|
+
test 'find_by_param' do
|
62
|
+
assert_equal credentials(:john_token), Credentials::Token.
|
63
|
+
find_by_param(credentials(:john_token).to_param)
|
64
|
+
assert_equal credentials(:jane_token), Credentials::Token.
|
65
|
+
find_by_param(credentials(:jane_token).to_param)
|
66
|
+
assert_equal nil, Credentials::Token.find_by_param('bogus token')
|
67
|
+
assert_equal nil, Credentials::Token.find_by_param(nil)
|
68
|
+
end
|
69
|
+
|
70
|
+
test 'class authenticate' do
|
71
|
+
john = 'YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A'
|
72
|
+
jane = '6TXe1vv7BgOw0BkJ1hzUKO6G08fLk4sVfJ3wPDZHS-c'
|
73
|
+
bogus = 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo'
|
74
|
+
assert_equal users(:john), Credentials::Token.authenticate(john)
|
75
|
+
assert_equal users(:jane), Credentials::Token.authenticate(jane)
|
76
|
+
assert_equal :invalid, Credentials::Token.authenticate(bogus)
|
77
|
+
end
|
78
|
+
|
79
|
+
test 'class authenticate calls User#auth_bounce_reason' do
|
80
|
+
john = 'YZ-Fo8HX6_NyU6lVZXYi6cMDLV5eAgt35UTF5l8bD6A'
|
81
|
+
jane = '6TXe1vv7BgOw0BkJ1hzUKO6G08fLk4sVfJ3wPDZHS-c'
|
82
|
+
bogus = 'AyCMIixa5C7BBqU-XFI7l7IaUFJ4zQZPmcK6oNb3FLo'
|
83
|
+
|
84
|
+
with_blocked_credential credentials(:john_token), :reason do
|
85
|
+
assert_equal :reason, Credentials::Token.authenticate(john)
|
86
|
+
assert_equal users(:jane), Credentials::Token.authenticate(jane)
|
87
|
+
assert_equal :invalid, Credentials::Token.authenticate(bogus)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
test 'instance authenticate' do
|
92
|
+
assert_equal users(:john), credentials(:john_token).authenticate
|
93
|
+
assert_equal users(:jane), credentials(:jane_token).authenticate
|
94
|
+
end
|
95
|
+
|
96
|
+
test 'instance authenticate calls User#auth_bounce_reason' do
|
97
|
+
with_blocked_credential credentials(:john_token), :reason do
|
98
|
+
assert_equal :reason, credentials(:john_token).authenticate
|
99
|
+
assert_equal users(:jane), credentials(:jane_token).authenticate
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<p>
|
2
|
+
This view gets displayed when the user tries to access something forbidden.
|
3
|
+
</p>
|
4
|
+
|
5
|
+
<% if current_user %>
|
6
|
+
<p>
|
7
|
+
You should inform the user that they are logged in as
|
8
|
+
<%= current_user.exuid %> and suggest them to
|
9
|
+
<%= link_to 'Log out', session_path, :method => :destroy %> and log in as a
|
10
|
+
different user.
|
11
|
+
</p>
|
12
|
+
<% else %>
|
13
|
+
<p>
|
14
|
+
The user will only see this if JavaScript is disabled. Ask them to
|
15
|
+
<%= link_to 'Log in', new_session_path %>.
|
16
|
+
</p>
|
17
|
+
<script type="text/javascript">
|
18
|
+
window.location = "<%= new_session_path %>";
|
19
|
+
</script>
|
20
|
+
<% end %>
|