From 10bc72acc200085a7757e6fa411916cb4e2ab7be Mon Sep 17 00:00:00 2001 From: Rei Date: Sat, 12 Apr 2014 16:24:11 +0800 Subject: [PATCH] Add user confirmation module --- app/assets/stylesheets/users.css.scss | 13 +++ .../users/confirmations_controller.rb | 33 +++++++ app/mailers/user_mailer.rb | 6 ++ app/models/concerns/user/confirmable.rb | 22 +++++ app/models/user.rb | 1 + app/views/layouts/application.html.slim | 2 + .../share/_user_confirm_required.html.slim | 19 +++++ app/views/user_mailer/confirmation.html.slim | 1 + app/views/user_mailer/confirmation.text.erb | 1 + app/views/users/confirmations/create.js.erb | 1 + app/views/users/confirmations/limiter.js.erb | 1 + app/views/users/confirmations/show.html.slim | 10 +++ config/i18n-tasks.yml | 1 + config/locales/en.yml | 85 ++++++++++++------- config/locales/zh-CN.yml | 59 ++++++++----- config/routes.rb | 1 + .../20140412065000_add_confirmed_to_users.rb | 5 ++ db/structure.sql | 5 +- .../users/confirmations_controller_test.rb | 48 +++++++++++ test/mailers/previews/user_mailer_preview.rb | 5 ++ test/mailers/user_mailer_test.rb | 9 +- test/models/user_test.rb | 11 +++ 22 files changed, 286 insertions(+), 53 deletions(-) create mode 100644 app/controllers/users/confirmations_controller.rb create mode 100644 app/models/concerns/user/confirmable.rb create mode 100644 app/views/share/_user_confirm_required.html.slim create mode 100644 app/views/user_mailer/confirmation.html.slim create mode 100644 app/views/user_mailer/confirmation.text.erb create mode 100644 app/views/users/confirmations/create.js.erb create mode 100644 app/views/users/confirmations/limiter.js.erb create mode 100644 app/views/users/confirmations/show.html.slim create mode 100644 db/migrate/20140412065000_add_confirmed_to_users.rb create mode 100644 test/controllers/users/confirmations_controller_test.rb diff --git a/app/assets/stylesheets/users.css.scss b/app/assets/stylesheets/users.css.scss index 273f0ee..6bdf4d9 100644 --- a/app/assets/stylesheets/users.css.scss +++ b/app/assets/stylesheets/users.css.scss @@ -1,3 +1,16 @@ +#user-confirm-required { + padding: $line-height-computed 0; + background: white; + + .alert { + margin: 0; + } + + .modal-header { + border-bottom: none; + } +} + .user-profile { background: white; padding: 20px 0; diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 0000000..ff49430 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,33 @@ +class Users::ConfirmationsController < ApplicationController + before_action :login_required + before_action :access_limiter, only: [:create] + + def show + if params[:token].present? + @user = User.find_by_confirmation_token(params[:token]) + if @user && @user == current_user + @user.confirm + flash[:success] = I18n.t('users.confirmations.confirm_success') + redirect_to settings_profile_url + end + end + end + + def create + UserMailer.confirmation(current_user.id).deliver + end + + private + + def access_limiter + key = "verifies:limiter:#{request.remote_ip}" + if $redis.get(key).to_i > 0 + render :limiter + else + $redis.incr(key) + if $redis.ttl(key) == -1 + $redis.expire(key, 60) + end + end + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 54df38a..b520ca7 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -9,4 +9,10 @@ class UserMailer < ActionMailer::Base mail(to: @user.email, subject: I18n.t('user_mailer.password_reset.subject')) end + + def confirmation(user_id) + @user = User.find(user_id) + mail(to: @user.email, + subject: I18n.t('user_mailer.confirmation.subject')) + end end diff --git a/app/models/concerns/user/confirmable.rb b/app/models/concerns/user/confirmable.rb new file mode 100644 index 0000000..810203e --- /dev/null +++ b/app/models/concerns/user/confirmable.rb @@ -0,0 +1,22 @@ +class User + module Confirmable + extend ActiveSupport::Concern + + def confirm + update_attribute :confirmed, true + end + + def confirmation_token + self.class.verifier_for('confirmation').generate([id, Time.now]) + end + + module ClassMethods + def find_by_confirmation_token(token) + user_id, timestamp = verifier_for('confirmation').verify(token) + User.find_by(id: user_id) if timestamp > 1.hour.ago + rescue ActiveSupport::MessageVerifier::InvalidSignature + nil + end + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 09edeed..9fb3163 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,4 +1,5 @@ class User < ActiveRecord::Base + include Confirmable include Gravtastic gravtastic secure: true, default: 'wavatar', rating: 'G', size: 48 mount_uploader :avatar, AvatarUploader diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 3969c17..9ef5459 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -72,4 +72,6 @@ html a href=topics_path = t '.community' + - if login? and !current_user.confirmed? + = render 'share/user_confirm_required' = yield diff --git a/app/views/share/_user_confirm_required.html.slim b/app/views/share/_user_confirm_required.html.slim new file mode 100644 index 0000000..30f3313 --- /dev/null +++ b/app/views/share/_user_confirm_required.html.slim @@ -0,0 +1,19 @@ +#user-confirm-required + .container + .alert.alert-warning + p + = t '.description', email: current_user.email + p + a.btn.btn-default href=users_confirmation_path data-remote="true" data-method="post" + = t '.resend_email' + ' + a.btn href=settings_account_path + = t '.change_email' + + #user-confirm-required-modal.modal.fade + .modal-dialog + .modal-content + .modal-header + button.close data-dismiss="modal" + | × + .modal-title diff --git a/app/views/user_mailer/confirmation.html.slim b/app/views/user_mailer/confirmation.html.slim new file mode 100644 index 0000000..2ea11ba --- /dev/null +++ b/app/views/user_mailer/confirmation.html.slim @@ -0,0 +1 @@ += sanitize markdown t '.text_body', url: users_confirmation_url(token: @user.confirmation_token), host: CONFIG['host'] diff --git a/app/views/user_mailer/confirmation.text.erb b/app/views/user_mailer/confirmation.text.erb new file mode 100644 index 0000000..7c2ef76 --- /dev/null +++ b/app/views/user_mailer/confirmation.text.erb @@ -0,0 +1 @@ +<%= t '.text_body', url: users_confirmation_url(token: @user.confirmation_token), host: CONFIG['host'] %> diff --git a/app/views/users/confirmations/create.js.erb b/app/views/users/confirmations/create.js.erb new file mode 100644 index 0000000..7780b0a --- /dev/null +++ b/app/views/users/confirmations/create.js.erb @@ -0,0 +1 @@ +$('#user-confirm-required-modal').find('.modal-title').text('<%= t '.resend_success' %>').end().modal(); diff --git a/app/views/users/confirmations/limiter.js.erb b/app/views/users/confirmations/limiter.js.erb new file mode 100644 index 0000000..a098f77 --- /dev/null +++ b/app/views/users/confirmations/limiter.js.erb @@ -0,0 +1 @@ +$('#user-confirm-required-modal').find('.modal-title').text('<%= t '.resend_limiter' %>').end().modal(); diff --git a/app/views/users/confirmations/show.html.slim b/app/views/users/confirmations/show.html.slim new file mode 100644 index 0000000..3c36471 --- /dev/null +++ b/app/views/users/confirmations/show.html.slim @@ -0,0 +1,10 @@ +.main + .container + .row + .col-md-8.col-md-push-2 + .panel + .panel-heading + h3.panel-title + = t '.confirm_fail' + .panel-body + = t '.confirmation_toke_invalid_or_expired_please_resend' diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 4e7bbec..99d1ed3 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -6,6 +6,7 @@ search: - '*.rb' - '*.slim' - '*.text.erb' + - '*.js.erb' ignore_unused: - activerecord.* diff --git a/config/locales/en.yml b/config/locales/en.yml index 4935edd..29560a3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -6,9 +6,19 @@ en: description: Description name: Name slug: Slug + topics_count: Topics count + comment: + commentable: Commentable + created_at: Created at + user: User + topic: + comments_count: Comment count + created_at: Created at + user: User user: avatar: Avatar bio: Bio + created_at: Created at email: Email locale: Locale name: Name @@ -22,8 +32,7 @@ en: email: format: invalid email format username: - format: may only contain alphanumeric characters or dashes and cannot - begin with a dash + format: may only contain alphanumeric characters or dashes and cannot begin with a dash admin: attachments: index: @@ -37,8 +46,7 @@ en: form: create_category: Create category permanently_delete: Permanently delete - permanently_delete_confirm: Are you sure you want to permanently delete this - category? + permanently_delete_confirm: Are you sure you want to permanently delete this category? save_changes: Save changes index: categories: Categories @@ -92,8 +100,7 @@ en: show: lock: Lock permanently_delete: Permanently delete - permanently_delete_confirm: Are you sure you want to permanently delete this - user? + permanently_delete_confirm: Are you sure you want to permanently delete this user? remove_avatar: Remove avatar save_changes: Save changes unlock: Unlock @@ -148,6 +155,11 @@ en: commented_on: Commented on mention: mentioned_you_on: Mentioned you on + passwords: + flashes: + successfully_update: Successfully update + token_invalid: Token invalid + user_email_not_found: User email not found sessions: access_limiter: are_you: Are you @@ -191,6 +203,11 @@ en: account: Account password: Password profile: Profile + share: + user_confirm_required: + change_email: Change email + description: "Please confirm your email for access all function. Confirm email has been send to %{email} ." + resend_email: Resend email subscriptions: subscription: ignoring: Ignoring @@ -200,10 +217,8 @@ en: watch: Watch watching: Watching you_do_not_receive_any_notifications: You do not receive any notifications - you_only_receive_notifications_if_you_are_mentioned: You only receive notifications - if you are mentioned - you_will_receive_notifications_for_all_comments: You will receive notifications - for all comments + you_only_receive_notifications_if_you_are_mentioned: You only receive notifications if you are mentioned + you_will_receive_notifications_for_all_comments: You will receive notifications for all comments topics: edit: edit_topic: Edit topic @@ -242,34 +257,23 @@ en: topics_header: create_topic: Create topic user_mailer: + confirmation: + subject: Email confirmation + text_body: | + Somebody sign up in %{host} with your email. If it was not your, safely ignore this email. + + Click the following link to choose a new password: + + %{url} password_reset: subject: Password reset text_body: | - Somebody asked to reset your password on %{host} . - - If it was not you, you can safely ignore this email. + Somebody asked to reset your password on %{host}. If it was not you, safely ignore this email. Click the following link to choose a new password: %{url} users: - passwords: - edit: - password_reset: Password reset - save_changes: Save changes - flashes: - successfully_update: Successfully update - token_invalid: Token invalid - user_email_not_found: User email not found - new: - email: Email - password_reset: Password reset - send_reset_email: Send reset email - your_email: Your email - show: - password_reset: Password reset - password_reset_email_has_been_sent_message: Password reset email has been sent - message comments: index: comments: Comments @@ -277,6 +281,15 @@ en: no_comment_yet: No comment yet publish: Publish user_s_comments: "%{name}'s comments" + confirmations: + confirm_success: Confirm success + create: + resend_success: Resend success + limiter: + resend_limiter: Resend limiter + show: + confirm_fail: Confirm fail + confirmation_toke_invalid_or_expired_please_resend: Confirmation toke invalid or expired, please resend. new: choose_a_password: Choose a password create_account: Create account @@ -284,6 +297,18 @@ en: sign_up: Sign up your_email: Your email your_full_name: Your full name + passwords: + edit: + password_reset: Password reset + save_changes: Save changes + new: + email: Email + password_reset: Password reset + send_reset_email: Send reset email + your_email: Your email + show: + password_reset: Password reset + password_reset_email_has_been_sent_message: Password reset email has been sent. profile: edit_profile: Edit profile sub_navbar: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 4e81f2d..92ac85b 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -155,6 +155,11 @@ zh-CN: commented_on: "评论了" mention: mentioned_you_on: "提及你于" + passwords: + flashes: + successfully_update: "密码更新成功" + token_invalid: "验证码无效" + user_email_not_found: "没有找到此用户" sessions: access_limiter: are_you: "你是否" @@ -198,6 +203,11 @@ zh-CN: account: "帐号" password: "密码" profile: "个人资料" + share: + user_confirm_required: + change_email: "修改邮件地址" + description: "确认你的邮件地址方可访问所有功能。确认邮件已发往 %{email} 。" + resend_email: "重新发送邮件" subscriptions: subscription: ignoring: "忽略" @@ -247,33 +257,23 @@ zh-CN: topics_header: create_topic: "创建话题" user_mailer: + confirmation: + subject: "邮箱确认" + text_body: | + 有人使用你的邮箱在 %{host} 注册帐号,如果不是你,请忽略此邮件。 + + 点击下面的链接确认邮箱地址: + + %{url} password_reset: subject: "密码重置" text_body: | - 有人请求重置你在 %{host} 的密码。 - - 如果不是你,你可以直接忽略本邮件。 + 有人请求重置你在 %{host} 的密码,如果不是你,请忽略此邮件。 点击下面的链接来选择一个新密码: %{url} users: - passwords: - edit: - password_reset: "密码重置" - save_changes: "保存修改" - flashes: - successfully_update: "成功更新,现在可以使用新密码登录!" - token_invalid: "密码重置地址过期或无效,请重新发送重置邮件。" - user_email_not_found: "没有找到此用户。" - new: - email: Email - password_reset: "密码重置" - send_reset_email: "发送重置密码" - your_email: "你的 Email 地址" - show: - password_reset: "密码重置" - password_reset_email_has_been_sent_message: "密码重置邮件已发送,请注意查收。" comments: index: comments: "评论" @@ -281,6 +281,15 @@ zh-CN: no_comment_yet: "还没有评论" publish: "发布" user_s_comments: "%{name} 的评论" + confirmations: + confirm_success: "恭喜,你的邮件地址已确认成功。" + create: + resend_success: "确认邮件已发送。" + limiter: + resend_limiter: "一分钟只能发送一次确认邮件。" + show: + confirm_fail: "确认失败" + confirmation_toke_invalid_or_expired_please_resend: "确认码无效或已过期,请重新发送确认邮件。" likes: index: no_comment_yet: "还没有评论" @@ -291,6 +300,18 @@ zh-CN: sign_up: "注册" your_email: "你的邮箱地址" your_full_name: "你的全名" + passwords: + edit: + password_reset: "密码重置" + save_changes: "保存修改" + new: + email: Email + password_reset: "密码重置" + send_reset_email: "发送重置密码" + your_email: "你的 Email 地址" + show: + password_reset: "密码重置" + password_reset_email_has_been_sent_message: "密码重置邮件已发送,请注意查收。" profile: edit_profile: "编辑个人资料" sub_navbar: diff --git a/config/routes.rb b/config/routes.rb index 8405ef4..469056e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ Rails.application.routes.draw do namespace :users do resource :password, only: [:show, :new, :create, :edit, :update] + resource :confirmation, only: [:show, :create] end concern :commentable do diff --git a/db/migrate/20140412065000_add_confirmed_to_users.rb b/db/migrate/20140412065000_add_confirmed_to_users.rb new file mode 100644 index 0000000..447d29d --- /dev/null +++ b/db/migrate/20140412065000_add_confirmed_to_users.rb @@ -0,0 +1,5 @@ +class AddConfirmedToUsers < ActiveRecord::Migration + def change + add_column :users, :confirmed, :boolean, default: false + end +end diff --git a/db/structure.sql b/db/structure.sql index 3b5e083..8c86b7d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -295,7 +295,8 @@ CREATE TABLE users ( locale character varying(255), locked_at timestamp without time zone, created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + confirmed boolean DEFAULT false ); @@ -576,3 +577,5 @@ INSERT INTO schema_migrations (version) VALUES ('20140310070632'); INSERT INTO schema_migrations (version) VALUES ('20140405074043'); +INSERT INTO schema_migrations (version) VALUES ('20140412065000'); + diff --git a/test/controllers/users/confirmations_controller_test.rb b/test/controllers/users/confirmations_controller_test.rb new file mode 100644 index 0000000..a11df32 --- /dev/null +++ b/test/controllers/users/confirmations_controller_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class Users::ConfirmationsControllerTest < ActionController::TestCase + def setup + $redis.flushdb + end + + test "should confirm user if token valid" do + login_as create(:user, confirmed: false) + get :show, token: current_user.confirmation_token + assert current_user.reload.confirmed? + end + + test "should not confirm user if token invalid" do + login_as create(:user, confirmed: false) + get :show, token: current_user.confirmation_token[0..-2] + assert !current_user.reload.confirmed? + end + + test "should show" do + login_as create(:user, confirmed: false) + get :show + assert_response :success, @response.body + end + + test "should create confirm" do + login_as create(:user, confirmed: false) + xhr :post, :create + assert ActionMailer::Base.deliveries.any? + end + + test "should access limit" do + login_as create(:user) + ip = '1.2.3.4' + key = "verifies:limiter:#{ip}" + request.headers['REMOTE_ADDR'] = ip + assert_equal nil, $redis.get(key) + assert_difference "ActionMailer::Base.deliveries.count" do + xhr :post, :create + assert_equal 1, $redis.get(key).to_i + end + + assert_no_difference "ActionMailer::Base.deliveries.count" do + xhr :post, :create + assert_equal 1, $redis.get(key).to_i + end + end +end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb index e2b8295..f558190 100644 --- a/test/mailers/previews/user_mailer_preview.rb +++ b/test/mailers/previews/user_mailer_preview.rb @@ -4,4 +4,9 @@ class UserMailerPreview < ActionMailer::Preview user = User.first || FactoryGirl.create(:user) UserMailer.password_reset(user.id) end + + def confirmation + user = User.first || FactoryGirl.create(:user) + UserMailer.confirmation(user.id) + end end diff --git a/test/mailers/user_mailer_test.rb b/test/mailers/user_mailer_test.rb index 67a1629..c5689c4 100644 --- a/test/mailers/user_mailer_test.rb +++ b/test/mailers/user_mailer_test.rb @@ -1,7 +1,10 @@ require 'test_helper' class UserMailerTest < ActionMailer::TestCase - # test "the truth" do - # assert true - # end + test "confirmation" do + user = create(:user) + email = UserMailer.confirmation(user.id).deliver + assert ActionMailer::Base.deliveries.any? + assert_equal [user.email], email.to + end end diff --git a/test/models/user_test.rb b/test/models/user_test.rb index 1739ea9..08deec3 100644 --- a/test/models/user_test.rb +++ b/test/models/user_test.rb @@ -22,4 +22,15 @@ class UserTest < ActiveSupport::TestCase token = user.password_reset_token assert_equal user, User.find_by_password_reset_token(token) end + + test "shuold generate confirmation token" do + user = create(:user) + assert_not_nil user.confirmation_token + end + + test "should find_by_confirmation_token" do + user = create(:user) + token = user.confirmation_token + assert_equal user, User.find_by_confirmation_token(token) + end end