diff --git a/app/controllers/maintenance/user/login_reminders_controller.rb b/app/controllers/maintenance/user/login_reminders_controller.rb new file mode 100644 index 000000000..15b043798 --- /dev/null +++ b/app/controllers/maintenance/user/login_reminders_controller.rb @@ -0,0 +1,20 @@ +module Maintenance + module User + class LoginRemindersController < ApplicationController + def new + end + + def create + @user = ::User.with_email(params[:user][:email]).first + if @user + LoginReminderMailer.notice(@user).deliver + flash[:notice] = "Email sent" + else + flash[:notice] = "Email address not found" + end + + redirect_to new_maintenance_user_login_reminder_path + end + end + end +end diff --git a/app/controllers/maintenance/user/password_resets_controller.rb b/app/controllers/maintenance/user/password_resets_controller.rb new file mode 100644 index 000000000..22116c39b --- /dev/null +++ b/app/controllers/maintenance/user/password_resets_controller.rb @@ -0,0 +1,34 @@ +module Maintenance + module User + class PasswordResetsController < ApplicationController + def new + @nonce = UserPasswordResetNonce.new + end + + def create + @nonce = UserPasswordResetNonce.create(params[:nonce]) + if @nonce.errors.any? + redirect_to new_maintenance_user_password_reset_path, :notice => @nonce.errors.full_messages.join("; ") + else + redirect_to new_maintenance_user_password_reset_path, :notice => "Email request sent" + end + end + + def edit + @nonce = UserPasswordResetNonce.where(:email => params[:email], :key => params[:key]).first + end + + def update + @nonce = UserPasswordResetNonce.where(:email => params[:email], :key => params[:key]).first + + if @nonce + @nonce.reset_user! + @nonce.destroy + redirect_to new_maintenance_user_password_reset_path, :notice => "Password reset; email delivered with new password" + else + redirect_to new_maintenance_user_password_reset_path, :notice => "Invalid key" + end + end + end + end +end diff --git a/app/controllers/user_maintenance_controller.rb b/app/controllers/user_maintenance_controller.rb deleted file mode 100644 index ea480db65..000000000 --- a/app/controllers/user_maintenance_controller.rb +++ /dev/null @@ -1,25 +0,0 @@ -class UserMaintenanceController < ApplicationController - def login_reminder - if request.post? - @user = User.with_email(params[:user][:email]).first - if @user - UserMaintenanceMailer.login_reminder(@user).deliver - flash[:notice] = "Email sent" - else - flash[:notice] = "No matching user record found" - end - end - end - - def reset_password - if request.post? - @user = User.find_for_password_reset(params[:user][:name], params[:user][:email]).first - if @user - @user.reset_password_and_deliver_notice - flash[:notice] = "Email sent" - else - flash[:notice] = "No matching user record found" - end - end - end -end diff --git a/app/controllers/wiki_page_versions_controller.rb b/app/controllers/wiki_page_versions_controller.rb index 7645e87d4..935895ecf 100644 --- a/app/controllers/wiki_page_versions_controller.rb +++ b/app/controllers/wiki_page_versions_controller.rb @@ -3,7 +3,7 @@ class WikiPageVersionsController < ApplicationController def index @search = WikiPageVersion.search(params[:search]) - @wiki_page_versions = @search.paginate(:page => params[:page]) + @wiki_page_versions = @search.paginate(params[:page]) respond_with(@wiki_page_versions) end diff --git a/app/logical/post_sets/artist.rb b/app/logical/post_sets/artist.rb index cccb64013..cdd822f8f 100644 --- a/app/logical/post_sets/artist.rb +++ b/app/logical/post_sets/artist.rb @@ -6,5 +6,13 @@ module PostSets super(:tags => artist.name) @artist = artist end + + def posts + ::Post.tag_match(@artist.name) + end + + def presenter + ::PostSetPresenters::Post.new(self) + end end end diff --git a/app/mailers/maintenance/user/login_reminder_mailer.rb b/app/mailers/maintenance/user/login_reminder_mailer.rb new file mode 100644 index 000000000..76beac06e --- /dev/null +++ b/app/mailers/maintenance/user/login_reminder_mailer.rb @@ -0,0 +1,10 @@ +module Maintenance + module User + class LoginReminderMailer < ActionMailer::Base + def notice(user) + @user = user + mail(:to => user.email, :subject => "#{Danbooru.config.app_name} login reminder") + end + end + end +end diff --git a/app/mailers/maintenance/user/password_reset_mailer.rb b/app/mailers/maintenance/user/password_reset_mailer.rb new file mode 100644 index 000000000..264248a4e --- /dev/null +++ b/app/mailers/maintenance/user/password_reset_mailer.rb @@ -0,0 +1,15 @@ +module Maintenance + module User + class PasswordResetMailer < ActionMailer::Base + def request(user) + @user = user + mail(:to => @user.email, :subject => "#{Danbooru.config.app_name} password reset request") + end + + def confirmation(user) + @user = user + mail(:to => @user.email, :subject => "#{Danbooru.config.app_name} password reset confirmation") + end + end + end +end diff --git a/app/models/user_maintenance_mailer.rb b/app/models/user_maintenance_mailer.rb index 78905b4f2..5882caab9 100644 --- a/app/models/user_maintenance_mailer.rb +++ b/app/models/user_maintenance_mailer.rb @@ -1,10 +1,6 @@ class UserMaintenanceMailer < ActionMailer::Base default :from => Danbooru.config.contact_email - def login_reminder(user) - @user = user - mail(:to => user.email, :subject => "#{Danbooru.config.app_name} login reminder") - end def reset_password(user, new_password) @user = user diff --git a/app/models/user_password_reset_nonce.rb b/app/models/user_password_reset_nonce.rb new file mode 100644 index 000000000..8877680b1 --- /dev/null +++ b/app/models/user_password_reset_nonce.rb @@ -0,0 +1,30 @@ +class UserPasswordResetNonce < ActiveRecord::Base + validates_uniqueness_of :email + validates_presence_of :email, :key + validate :validate_existence_of_email + before_validation :initialize_key, :on => :create + after_create :deliver_notice + + def deliver_notice + Maintenance::User::PasswordResetMailer.request(user).deliver + end + + def initialize_key + self.key = SecureRandom.hex(16) + end + + def validate_existence_of_email + if !User.with_email(email).exists? + errors[:email] << "is invalid" + return false + end + end + + def reset_user! + user.reset_password_and_deliver_notice + end + + def user + @user ||= User.with_email(email).first + end +end diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 1504f960e..b4cc8da2a 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -82,7 +82,7 @@ class WikiPage < ActiveRecord::Base end def post_set - @post_set ||= PostSets::WikiPage.new(title) + @post_set ||= PostSets::Post.new(title) end def presenter diff --git a/app/views/maintenance/user/login_reminder_mailer/notice.html.erb b/app/views/maintenance/user/login_reminder_mailer/notice.html.erb new file mode 100644 index 000000000..d73075396 --- /dev/null +++ b/app/views/maintenance/user/login_reminder_mailer/notice.html.erb @@ -0,0 +1 @@ +

Your username is <%= @user.name %>.

\ No newline at end of file diff --git a/app/views/maintenance/user/login_reminders/new.html.erb b/app/views/maintenance/user/login_reminders/new.html.erb new file mode 100644 index 000000000..bde3a3a3e --- /dev/null +++ b/app/views/maintenance/user/login_reminders/new.html.erb @@ -0,0 +1,9 @@ +
+
+

Login Reminder

+ +

If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can email you your login information. Password details will not be provided and will not be changed.

+ +

If you didn't supply a valid email address, you are out of luck.

+
+
diff --git a/app/views/maintenance/user/password_resets/edit.html.erb b/app/views/maintenance/user/password_resets/edit.html.erb new file mode 100644 index 000000000..d5757696b --- /dev/null +++ b/app/views/maintenance/user/password_resets/edit.html.erb @@ -0,0 +1,14 @@ +
+
+

Reset Password

+ + <% if @nonce %> + <%= form_tag(maintenance_user_password_reset_path, :method => :put) do %> +

Do you wish to reset your password? A new password will be emailed to you.

+ <%= submit_tag "Reset" %> + <% end %> + <% else %> +

Invalid key

+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/maintenance/user/password_resets/new.html.erb b/app/views/maintenance/user/password_resets/new.html.erb new file mode 100644 index 000000000..d26d6bce4 --- /dev/null +++ b/app/views/maintenance/user/password_resets/new.html.erb @@ -0,0 +1,14 @@ +
+
+

Reset Password

+ +

If you supplied an email address when signing up, <%= Danbooru.config.app_name %> can reset your password. You will receive an email confirming your request for a new password.

+ +

If you didn't supply a valid email address, you are out of luck.

+ + <%= form_tag(maintenance_user_password_reset_path, :method => :post) do %> + <%= text_field :nonce, :email %> + <%= submit_tag "Submit" %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index ca52af5c3..073afcce1 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -4,12 +4,6 @@ <% simple_form_for(@search) do |f| %> <%= f.input :name_contains, :label => "Name" %> - <%= f.input :is_banned_is_true, :label => "Banned" %> - <%= f.input :is_privileged_is_true, :label => "Privileged" %> - <%= f.input :is_contributor_is_true, :label => "Contributor" %> - <%= f.input :is_janitor_is_true, :label => "Janitor" %> - <%= f.input :is_moderator_is_true, :label => "Moderator" %> - <%= f.input :is_admin_is_true, :label => "Admin" %> <%= f.sort_link "Name", :name %> <%= f.sort_link "Date", :created_at_desc %> <%= f.button :submit %> @@ -47,18 +41,18 @@ <% end %> <%= link_to user.note_versions.count, note_versions_path(:search => {:updater_id_eq => user.id}) %> - <%= user.pretty_level %> + <%= user.level_string %> <%= time_ago_in_words user.created_at %> ago <% end %> -
- <%= will_paginate(@users) %> +
+ <%= numbered_paginator(@users) %>
- <%= render :partial => "footer" %> + <%= render "secondary_links" %>
diff --git a/app/views/wiki_pages/show.html.erb b/app/views/wiki_pages/show.html.erb index 514c5721a..0d70bb055 100644 --- a/app/views/wiki_pages/show.html.erb +++ b/app/views/wiki_pages/show.html.erb @@ -10,7 +10,7 @@
- <%= @wiki_page.post_set.presenter.post_previews_html %> + <%= @wiki_page.post_set.presenter.post_previews_html(self) %>
diff --git a/config/application.rb b/config/application.rb index 956963461..adbdc8097 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,7 +12,7 @@ module Danbooru config.encoding = "utf-8" config.filter_parameters += [:password] config.assets.enabled = true - config.autoload_paths += %W(#{config.root}/app/presenters #{config.root}/app/logical) + config.autoload_paths += %W(#{config.root}/app/presenters #{config.root}/app/logical #{config.root}/app/mailers) config.plugins = [:all] config.time_zone = 'Eastern Time (US & Canada)' # config.action_view.javascript_expansions[:defaults] = [ diff --git a/db/development_structure.sql b/db/development_structure.sql index e59fa525a..d52b2ef10 100644 --- a/db/development_structure.sql +++ b/db/development_structure.sql @@ -2422,6 +2422,38 @@ CREATE SEQUENCE user_feedback_id_seq ALTER SEQUENCE user_feedback_id_seq OWNED BY user_feedback.id; +-- +-- Name: user_password_reset_nonces; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE user_password_reset_nonces ( + id integer NOT NULL, + key character varying(255) NOT NULL, + email character varying(255) NOT NULL, + created_at timestamp without time zone, + updated_at timestamp without time zone +); + + +-- +-- Name: user_password_reset_nonces_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE user_password_reset_nonces_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + + +-- +-- Name: user_password_reset_nonces_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE user_password_reset_nonces_id_seq OWNED BY user_password_reset_nonces.id; + + -- -- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: -- @@ -2758,6 +2790,13 @@ ALTER TABLE uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regcla ALTER TABLE user_feedback ALTER COLUMN id SET DEFAULT nextval('user_feedback_id_seq'::regclass); +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE user_password_reset_nonces ALTER COLUMN id SET DEFAULT nextval('user_password_reset_nonces_id_seq'::regclass); + + -- -- Name: id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3027,6 +3066,14 @@ ALTER TABLE ONLY user_feedback ADD CONSTRAINT user_feedback_pkey PRIMARY KEY (id); +-- +-- Name: user_password_reset_nonces_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_password_reset_nonces + ADD CONSTRAINT user_password_reset_nonces_pkey PRIMARY KEY (id); + + -- -- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: -- @@ -5122,4 +5169,6 @@ INSERT INTO schema_migrations (version) VALUES ('20110328215652'); INSERT INTO schema_migrations (version) VALUES ('20110328215701'); -INSERT INTO schema_migrations (version) VALUES ('20110607194023'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20110607194023'); + +INSERT INTO schema_migrations (version) VALUES ('20110717010705'); \ No newline at end of file diff --git a/db/migrate/20110717010705_create_user_password_reset_nonces.rb b/db/migrate/20110717010705_create_user_password_reset_nonces.rb new file mode 100644 index 000000000..55e83b0cc --- /dev/null +++ b/db/migrate/20110717010705_create_user_password_reset_nonces.rb @@ -0,0 +1,9 @@ +class CreateUserPasswordResetNonces < ActiveRecord::Migration + def change + create_table :user_password_reset_nonces do |t| + t.column :key, :string, :null => false + t.column :email, :string, :null => false + t.timestamps + end + end +end diff --git a/test/factories/user_password_reset_nonce.rb b/test/factories/user_password_reset_nonce.rb new file mode 100644 index 000000000..d1cd0076d --- /dev/null +++ b/test/factories/user_password_reset_nonce.rb @@ -0,0 +1,2 @@ +Factory.define(:user_password_reset_nonce) do |f| +end diff --git a/test/fixtures/user_password_reset_nonces.yml b/test/fixtures/user_password_reset_nonces.yml new file mode 100644 index 000000000..5394fa384 --- /dev/null +++ b/test/fixtures/user_password_reset_nonces.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/functional/maintenance/user/login_reminders_controller_test.rb b/test/functional/maintenance/user/login_reminders_controller_test.rb new file mode 100644 index 000000000..99e97c1dd --- /dev/null +++ b/test/functional/maintenance/user/login_reminders_controller_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +module Maintenance + module User + class LoginRemindersControllerTest < ActionController::TestCase + context "A login reminder controller" do + setup do + @user = Factory.create(:user) + @blank_email_user = Factory.create(:user, :email => "") + CurrentUser.user = nil + CurrentUser.ip_addr = "127.0.0.1" + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.deliveries.clear + end + + teardown do + CurrentUser.user = nil + CurrentUser.ip_addr = nil + end + + should "render the new page" do + get :new + assert_response :success + end + + should "deliver an email with the login to the user" do + post :create, {:user => {:email => @user.email}} + assert_equal(flash[:notice], "Email sent") + assert_equal(1, ActionMailer::Base.deliveries.size) + end + + context "for a user with a blank email" do + should "fail" do + post :create, {:user => {:email => ""}} + assert_equal("Email address not found", flash[:notice]) + @blank_email_user.reload + assert_equal(@blank_email_user.created_at, @blank_email_user.updated_at) + assert_equal(0, ActionMailer::Base.deliveries.size) + end + end + end + end + end +end diff --git a/test/functional/maintenance/user/password_resets_controller_test.rb b/test/functional/maintenance/user/password_resets_controller_test.rb new file mode 100644 index 000000000..1c43d8f1e --- /dev/null +++ b/test/functional/maintenance/user/password_resets_controller_test.rb @@ -0,0 +1,98 @@ +require "test_helper" + +module Maintenance + module User + class PasswordResetsControllerTest < ActionController::TestCase + context "A password resets controller" do + setup do + @user = Factory.create(:user, :email => "abc@com.net") + CurrentUser.user = nil + CurrentUser.ip_addr = "127.0.0.1" + ActionMailer::Base.delivery_method = :test + ActionMailer::Base.deliveries.clear + end + + teardown do + CurrentUser.user = nil + CurrentUser.ip_addr = nil + end + + should "render the new page" do + get :new + assert_response :success + end + + context "create action" do + context "given invalid parameters" do + setup do + post :create, {:nonce => {:email => ""}} + end + + should "not create a new nonce" do + assert_equal(0, UserPasswordResetNonce.count) + end + + should "redirect to the new page" do + assert_redirected_to new_maintenance_user_password_reset_path + end + + should "not deliver an email" do + assert_equal(0, ActionMailer::Base.deliveries.size) + end + end + + context "given valid parameters" do + setup do + post :create, {:nonce => {:email => @user.email}} + end + + should "create a new nonce" do + assert_equal(1, UserPasswordResetNonce.where(:email => @user.email).count) + end + + should "redirect to the new page" do + assert_redirected_to new_maintenance_user_password_reset_path + end + + should "deliver an email to the supplied email address" do + assert_equal(1, ActionMailer::Base.deliveries.size) + end + end + end + + context "edit action" do + context "with invalid parameters" do + setup do + get :edit, :email => "a@b.c" + end + + should "succeed silently" do + assert_response :success + end + end + + context "with valid parameters" do + setup do + @user = Factory.create(:user) + @nonce = Factory.create(:user_password_reset_nonce, :email => @user.email) + ActionMailer::Base.deliveries.clear + post :update, :email => @nonce.email, :key => @nonce.key + end + + should "succeed" do + assert_redirected_to new_maintenance_user_password_reset_path + end + + should "send an email" do + assert_equal(1, ActionMailer::Base.deliveries.size) + end + + should "delete the nonce" do + assert_equal(0, UserPasswordResetNonce.count) + end + end + end + end + end + end +end diff --git a/test/functional/wiki_pages_controller_test.rb b/test/functional/wiki_pages_controller_test.rb index 88911709a..93c826c17 100644 --- a/test/functional/wiki_pages_controller_test.rb +++ b/test/functional/wiki_pages_controller_test.rb @@ -15,7 +15,8 @@ class WikiPagesControllerTest < ActionController::TestCase context "index action" do setup do - Factory.create(:wiki_page, :title => "abc") + @wiki_page_abc = Factory.create(:wiki_page, :title => "abc") + @wiki_page_def = Factory.create(:wiki_page, :title => "def") end should "list all wiki_pages" do @@ -25,7 +26,7 @@ class WikiPagesControllerTest < ActionController::TestCase should "list all wiki_pages (with search)" do get :index, {:search => {:title_matches => "abc"}} - assert_response :success + assert_redirected_to(wiki_page_path(@wiki_page_abc)) end end diff --git a/test/unit/maintenance/user/login_reminder_mailer_test.rb b/test/unit/maintenance/user/login_reminder_mailer_test.rb new file mode 100644 index 000000000..5e46a93c1 --- /dev/null +++ b/test/unit/maintenance/user/login_reminder_mailer_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +module Maintenance + module User + class LoginReminderMailerTest < ActionMailer::TestCase + context "The login reminder mailer" do + setup do + @user = Factory.create(:user) + end + + should "send the notie" do + LoginReminderMailer.notice(@user).deliver + assert !ActionMailer::Base.deliveries.empty? + end + end + end + end +end diff --git a/test/unit/user_password_reset_nonce_test.rb b/test/unit/user_password_reset_nonce_test.rb new file mode 100644 index 000000000..4133af6c1 --- /dev/null +++ b/test/unit/user_password_reset_nonce_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class UserPasswordResetNonceTest < ActiveSupport::TestCase + context "Creating a new nonce" do + context "with a valid email" do + setup do + @user = Factory.create(:user, :email => "aaa@b.net") + @nonce = Factory.create(:user_password_reset_nonce, :email => @user.email) + end + + should "validate" do + assert_equal([], @nonce.errors.full_messages) + end + + should "populate the key with a random string" do + assert_equal(32, @nonce.key.size) + end + + should "reset the password when reset" do + @nonce.user.expects(:reset_password_and_deliver_notice) + @nonce.reset_user! + end + end + + context "with a blank email" do + setup do + @user = Factory.create(:user, :email => "") + @nonce = UserPasswordResetNonce.new(:email => "") + end + + should "not validate" do + @nonce.save + assert_equal(["Email can't be blank", "Email is invalid"], @nonce.errors.full_messages.sort) + end + end + + context "with an invalid email" do + setup do + @nonce = UserPasswordResetNonce.new(:email => "z@z.net") + end + + should "not validate" do + @nonce.save + assert_equal(["Email is invalid"], @nonce.errors.full_messages) + end + end + end +end