diff --git a/Gemfile b/Gemfile index 973c847..1de7656 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ gem 'rails', '4.0.3' gem 'jquery-rails', '3.0.4' gem 'rails_autolink' gem 'mysql2' -gem 'authlogic' +gem 'devise' gem 'twitter_oauth', git: 'git://github.com/moomerman/twitter_oauth.git' gem 'therubyracer' gem 'exception_notification' diff --git a/Gemfile.lock b/Gemfile.lock index dd16c75..997314a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,13 +53,13 @@ GEM rake (>= 0.8.7) arel (4.0.2) atomic (1.1.14) - authlogic (3.3.0) - activerecord (>= 3.2) - activesupport (>= 3.2) autotest (4.4.6) ZenTest (>= 4.4.1) autotest-rails (4.2.1) ZenTest (~> 4.5) + bcrypt (3.1.7) + bcrypt-ruby (3.1.5) + bcrypt (>= 3.1.3) builder (3.1.4) capistrano (2.15.4) highline @@ -78,6 +78,12 @@ GEM daemons (1.1.9) dalli (2.7.0) database_cleaner (1.2.0) + devise (3.2.3) + bcrypt-ruby (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 3.2.6, < 5) + thread_safe (~> 0.1) + warden (~> 1.2.3) diff-lcs (1.2.5) dynamic_form (1.1.4) easy_translate (0.4.0) @@ -157,6 +163,7 @@ GEM nokogiri (1.6.1) mini_portile (~> 0.5.0) oauth (0.4.7) + orm_adapter (0.5.0) polyglot (0.3.4) quiet_assets (1.0.2) railties (>= 3.1, < 5.0) @@ -275,6 +282,8 @@ GEM json (>= 1.8.0) vegas (0.1.11) rack (>= 1.0.0) + warden (1.2.3) + rack (>= 1.0) webrat (0.7.3) nokogiri (>= 1.2.0) rack (>= 1.0) @@ -290,7 +299,6 @@ DEPENDENCIES acts_as_list acts_as_tree annotate (~> 2.6.1) - authlogic autotest autotest-rails capistrano (~> 2.15.4) @@ -298,6 +306,7 @@ DEPENDENCIES daemons dalli database_cleaner (~> 1.2.0) + devise dynamic_form exception_notification factory_girl_rails diff --git a/app/assets/javascripts/custom.js b/app/assets/javascripts/custom.js index 98b7d2b..3e89111 100644 --- a/app/assets/javascripts/custom.js +++ b/app/assets/javascripts/custom.js @@ -1,6 +1,19 @@ // when the document is ready $(document).ready(function() { + // allow flash notices to be dismissed + if ($(".flash").length > 0) { + $(".flash").on("click", function() { + $(this).hide("slow"); + }); + // hide flash automatically after 15 seconds + setTimeout(function() { + if ($(".flash").length > 0) { + $(".flash").hide("slow"); + } + }, 15000); + } + // show form to add a talkback command $('#talkback_command_add').click(function() { $(this).hide(); diff --git a/app/assets/stylesheets/custom.css b/app/assets/stylesheets/custom.css index 9d078bb..67324f3 100644 --- a/app/assets/stylesheets/custom.css +++ b/app/assets/stylesheets/custom.css @@ -7,6 +7,7 @@ body { padding-top: 70px; } .break-word { word-break: break-word; } .col-pad { padding: 0 15px; } +.dismiss { float: right; cursor: pointer; position: relative; top: -12px; left: 7px; } /* multiline forms */ .form-horizontal .multiline-label { margin-top: -10px; } @@ -229,10 +230,9 @@ input[type="text"].midfield { width: 120px; } textarea.tweet { margin-top: 0.5em; width: 40em; height: 3em; } /* error messages */ -.errorExplanation { width: 95%; background-color: #ffffe0; display: table; margin-bottom: 20px; padding: 10px; border: 1px solid #aaaaaa; } -#error { - color: red; -} +.errorExplanation, +#error_explanation { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; background-color: #f2dede; border-color: #ebccd1; color: #a94442; } +#error { color: red; } .field_with_errors { display: inline; } /*.error_box { margin-top: 15px; padding: 5px; background-color: #f99; color: #300; border: 1px solid #f66; }*/ .warning_box { margin: 15px 0 15px 0; padding: 10px; background-color: #fc3; color: #000; border: 1px solid #f90; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c1e8276..794f45e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,11 +1,14 @@ class ApplicationController < ActionController::Base + skip_before_filter :verify_authenticity_token # include all helpers for controllers helper :all # include these helper methods for views helper_method :current_user_session, :current_user, :logged_in?, :is_admin?, :get_header_value, :to_bytes protect_from_forgery before_filter :allow_cross_domain_access, :set_variables + before_filter :configure_permitted_parameters, if: :devise_controller? after_filter :remove_headers + before_filter :authenticate_user_from_token! # responds with blank def respond_with_blank @@ -44,6 +47,9 @@ class ApplicationController < ActionController::Base params[:sum] = '1440' if params[:sum] == 'daily' end + # change default devise sign_in page + def after_sign_in_path_for(resource); channels_path; end + # get the locale, but don't fail if header value doesn't exist def get_locale locale = get_header_value('HTTP_ACCEPT_LANGUAGE') @@ -59,8 +65,30 @@ class ApplicationController < ActionController::Base return locale end + protected + + def configure_permitted_parameters + devise_parameter_sanitizer.for(:sign_up) { |u| u.permit(:login, :email, :password, :password_confirmation, :remember_me) } + devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :email, :password, :remember_me) } + devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:login, :email, :password, :password_confirmation, :time_zone, :password_current) } + end + private + # authenticates user based on token from users#api_login + def authenticate_user_from_token! + # exit if no login or token + return false if params[:login].blank? || params[:token].blank? + + # get the user by login or email + user = User.find_by_login_or_email(params[:login]) + + # safe compare, avoids timing attacks + if user.present? && Devise.secure_compare(user.authentication_token, params[:token]) + sign_in user, store: false + end + end + # remove headers if necessary def remove_headers response.headers.delete_if {|key| true} if params[:headers] == 'false' @@ -80,7 +108,7 @@ class ApplicationController < ActionController::Base # check that user's email address matches admin def is_admin? - current_user && ADMIN_EMAILS.include?(current_user.email) + current_user.present? && ADMIN_EMAILS.include?(current_user.email) end def set_admin_menu @@ -104,16 +132,6 @@ class ApplicationController < ActionController::Base def set_plugins_menu; @menu = 'plugins'; end def set_devices_menu; @menu = 'devices'; end - def current_user_session - return @current_user_session if defined?(@current_user_session) - @current_user_session = UserSession.find - end - - def current_user - return @current_user if defined?(@current_user) - @current_user = current_user_session && current_user_session.record - end - def require_user logger.info "Require User" if current_user.nil? @@ -273,9 +291,6 @@ class ApplicationController < ActionController::Base # options: days = how many days ago, start = start date, end = end date, offset = timezone offset def get_date_range(params) - # set timezone correctly - set_time_zone(params) - # allow more past data if necessary get_old_data = (params[:results].present? || params[:start].present? || params[:days].present?) ? true : false @@ -290,8 +305,6 @@ class ApplicationController < ActionController::Base return date_range end - - def set_time_zone(params) # set timezone correctly if params[:offset] diff --git a/app/controllers/channels_controller.rb b/app/controllers/channels_controller.rb index 33abef6..089317d 100644 --- a/app/controllers/channels_controller.rb +++ b/app/controllers/channels_controller.rb @@ -35,20 +35,20 @@ class ChannelsController < ApplicationController # get channels by ids if params[:channel_ids].present? - flash[:notice] = t(:selected_channels) + @header = t(:selected_channels) @channels = Channel.public_viewable.by_array(params[:channel_ids]).order('ranking desc, updated_at DESC').paginate :page => params[:page] # get channels that match a user elsif params[:username].present? - flash[:notice] = "#{t(:user).capitalize}: #{params[:username]}" + @header = "#{t(:user).capitalize}: #{params[:username]}" searched_user = User.find_by_login(params[:username]) @channels = searched_user.channels.public_viewable.active.order('ranking desc, updated_at DESC').paginate :page => params[:page] if searched_user.present? # get channels that match a tag elsif params[:tag].present? - flash[:notice] = "#{t(:tag).capitalize}: #{params[:tag]}" + @header = "#{t(:tag).capitalize}: #{params[:tag]}" @channels = Channel.public_viewable.active.order('ranking desc, updated_at DESC').with_tag(params[:tag]).paginate :page => params[:page] # normal channel list else - flash[:notice] = t(:featured_channels) + @header = t(:featured_channels) respond_with_error(:error_resource_not_found) and return if params[:page] == '0' @channels = Channel.public_viewable.active.order('ranking desc, updated_at DESC').paginate :page => params[:page] end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index 7e29f5d..156b2c3 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -16,7 +16,7 @@ class CommentsController < ApplicationController @comment.body = params[:comment][:body].gsub(/<\/?[^>]*>/, '').gsub(/\n/, '
') # save comment if @comment.save - flash[:success] = "Thanks for adding a comment!" + flash[:notice] = "Thanks for adding a comment!" else flash[:error] = "Comment can't be blank!" end @@ -45,3 +45,4 @@ class CommentsController < ApplicationController redirect_to :back end end + diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 0000000..617219b --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,22 @@ +class RegistrationsController < Devise::RegistrationsController + include KeyUtilities + after_filter :add_api_key, :only => :create + + # use defaults from devise + def new; super; end + def new; super; end + def create; super; end + + private + + # adds an api key to the new user + def add_api_key + @user = current_user + if @user.present? + @user.api_key = generate_api_key(16, 'user') + @user.save + end + end + +end + diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..5d67b64 --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,32 @@ +class SessionsController < Devise::SessionsController + after_filter :log_failed_login, :only => :new + + # don't modify default devise controllers + def create; super; end + def new; super; end + + private + + # logs failed login attempts + def log_failed_login + if failed_login? + # log to failedlogins + failed = Failedlogin.new + failed.login = params['user']['login'] + failed.password = params['user']['password'] + failed.ip_address = get_header_value('X_REAL_IP') + failed.save + + # prevent timing and brute force password attacks + sleep 1 + end + end + + # true if a login fails + def failed_login? + options = env["warden.options"] + return (options.present? && options[:action] == "unauthenticated") + end + +end + diff --git a/app/controllers/user_sessions_controller.rb b/app/controllers/user_sessions_controller.rb deleted file mode 100644 index 630d422..0000000 --- a/app/controllers/user_sessions_controller.rb +++ /dev/null @@ -1,55 +0,0 @@ -class UserSessionsController < ApplicationController - before_filter :require_no_user, :only => [:new, :create] - before_filter :require_user, :only => :destroy - - def new - @title = t(:signin) - @user_session = UserSession.new - @mail_message = session[:mail_message] if !session[:mail_message].nil? - end - - def show - redirect_to root_path - end - - def create - - if params[:userlogin].length > 0 - render :text => '' - else - @user_session = UserSession.new(params[:user_session]) - - # remember user_id if checkbox is checked - if params[:user_session][:remember_id] == '1' - cookies['user_id'] = { :value => params[:user_session][:login], :expires => 1.month.from_now } - else - cookies.delete 'user_id' - end - - if @user_session.save - # if link_back, redirect back - redirect_to session[:link_back] and return if session[:link_back] - redirect_to channels_path and return - else - # log to failedlogins - failed = Failedlogin.new - failed.login = params[:user_session][:login] - failed.password = params[:user_session][:password] - failed.ip_address = get_header_value('X_REAL_IP') - failed.save - - # prevent timing and brute force password attacks - sleep 1 - @failed = true - render :action => :new - end - end - end - - def destroy - session[:link_back] = nil - current_user_session.destroy - reset_session - redirect_to root_path - end -end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2b0921b..fc4bc57 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,27 @@ class UsersController < ApplicationController include KeyUtilities - before_filter :require_no_user, :only => [:new, :create, :forgot_password] - before_filter :require_user, :only => [:show, :edit, :update, :change_password, :edit_profile] + skip_before_filter :verify_authenticity_token, :only => [:api_login] + before_filter :require_user, :only => [:show, :edit, :update, :edit_profile] + + # allow login via api + def api_login + # get the user by login or email + user = User.find_by_login_or_email(params[:login]) + + # exit if no user or invalid password + respond_with_error(:error_auth_required) and return if user.blank? || !user.valid_password?(params[:password]) + + # save new authentication token + user.authentication_token = Devise.friendly_token + user.save + + # output the user with token + respond_to do |format| + format.json { render :json => user.as_json(User.private_options_plus(:authentication_token)) } + format.xml { render :xml => user.to_xml(User.private_options_plus(:authentication_token)) } + format.any { render :text => user.authentication_token } + end + end # generates a new api key def new_api_key @@ -34,8 +54,8 @@ class UsersController < ApplicationController # if a json or xml request if request.format == :json || request.format == :xml - # authenticate the user if api key matches the target user - authenticated = (User.find_by_api_key(get_apikey) == @user) + # authenticate the user if the user is logged in (can be via token) or api key matches the target user + authenticated = (current_user == @user) || (User.find_by_api_key(get_apikey) == @user) # set options correctly options = authenticated ? User.private_options : User.public_options(@user) end @@ -75,27 +95,6 @@ class UsersController < ApplicationController end end - def new - @title = t(:signup) - @user = User.new - end - - def create - @user = User.new(user_params) - @user.api_key = generate_api_key(16, 'user') - - # save user - if @user.valid? - - if @user.save - redirect_back_or_default channels_path and return - end - else - render :action => :new - end - - end - def show @menu = 'account' @user = @current_user @@ -106,48 +105,18 @@ class UsersController < ApplicationController @user = @current_user end - # displays forgot password page - def forgot_password - @user = User.new - end - - # this action is called from an email link when a password reset is requested - def reset_password - # if user has been logged in (due to previous form submission) - if !current_user.nil? - @user = current_user - @user.errors.add(:base, t(:password_problem)) - @valid_link = true - else - @user = User.find_by_id(params[:id]) - # make sure tokens match and password reset is within last 10 minutes - if @user.perishable_token == params[:token] && @user.updated_at > 600.seconds.ago - @valid_link = true - # log the user in - @user_session = UserSession.new(@user) - @user_session.save - end - end - end - - # do the actual password change - def change_password - @user = current_user - # if no password entered, redirect - redirect_to reset_password_path and return if params[:user][:password].empty? - # check current password and update - if @user.update_attributes(user_params) - redirect_to account_path - else - redirect_to reset_password_path - end - end - def update @menu = 'account' @user = @current_user # makes our views "cleaner" and more consistent + + # delete password and confirmation from params if not present + params[:user].delete(:password) if params[:user][:password].blank? + # check current password and update - if @user.valid_password?(params[:password_current]) && @user.update_attributes(user_params) + if @user.valid_password?(params[:user][:password_current]) && @user.update_attributes(user_params) + # sign the user back in, since devise will log the user out on update + sign_in(current_user, :bypass => true) + flash[:notice] = t('devise.registrations.updated') redirect_to account_path else @user.errors.add(:base, t(:password_incorrect)) diff --git a/app/models/channel.rb b/app/models/channel.rb index d6bba02..12728de 100644 --- a/app/models/channel.rb +++ b/app/models/channel.rb @@ -81,6 +81,13 @@ class Channel < ActiveRecord::Base cattr_reader :per_page @@per_page = 15 + # how often the channel is updated + def update_rate + last_feeds = self.feeds.order('entry_id desc').limit(2) + rate = (last_feeds.first.created_at - last_feeds.last.created_at) if last_feeds.length == 2 + return rate + end + # write key for a channel def write_api_key self.api_keys.where(:write_flag => true).first.api_key diff --git a/app/models/user.rb b/app/models/user.rb index b184864..c20ee2e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,24 +2,27 @@ # # Table name: users # -# id :integer not null, primary key -# login :string(255) not null -# email :string(255) not null -# crypted_password :string(255) not null -# password_salt :string(255) not null -# persistence_token :string(255) not null -# perishable_token :string(255) not null -# current_login_at :datetime -# last_login_at :datetime -# current_login_ip :string(255) -# last_login_ip :string(255) -# created_at :datetime -# updated_at :datetime -# time_zone :string(255) -# public_flag :boolean default(FALSE) -# bio :text -# website :string(255) -# api_key :string(16) +# id :integer not null, primary key +# login :string(255) not null +# email :string(255) not null +# encrypted_password :string(255) not null +# password_salt :string(255) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# time_zone :string(255) +# public_flag :boolean default(FALSE) +# bio :text +# website :string(255) +# api_key :string(16) +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0), not null +# authentication_token :string(255) # ####### NOTE ####### @@ -28,6 +31,7 @@ #################### class User < ActiveRecord::Base include KeyUtilities + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable has_many :channels has_many :twitter_accounts, :dependent => :destroy has_many :thinghttps, :dependent => :destroy @@ -42,14 +46,45 @@ class User < ActiveRecord::Base has_many :watched_channels, :through => :watchings, :source => :channel has_many :comments - acts_as_authentic - self.include_root_in_json = false # pagination variables cattr_reader :per_page @@per_page = 50 + # allow login by login name also + def self.find_first_by_auth_conditions(warden_conditions) + conditions = warden_conditions.dup + if login_param = conditions.delete(:login) + where(conditions).where(["lower(login) = :value OR lower(email) = :value", { :value => login_param.downcase }]).first + else + where(conditions).first + end + end + + # allow users to sign in with passwords from old authlogic authentication + alias :devise_valid_password? :valid_password? + def valid_password?(password) + begin + devise_valid_password?(password) + rescue BCrypt::Errors::InvalidHash + stretches = 20 + digest = "#{password}#{self.password_salt}" + stretches.times {digest = Digest::SHA512.hexdigest(digest)} + if digest == self.encrypted_password + #Here update old Authlogic SHA512 Password with new Devise ByCrypt password + # SOURCE: https://github.com/plataformatec/devise/blob/master/lib/devise/models/database_authenticatable.rb + # Digests the password using bcrypt. + self.encrypted_password = self.password_digest(password) + self.save + return true + else + # If not BCryt password and not old Authlogic SHA512 password don't authenticate user + return false + end + end + end + # find a user using login or email def self.find_by_login_or_email(login) User.find_by_login(login) || User.find_by_email(login) @@ -81,6 +116,12 @@ class User < ActiveRecord::Base { :only => [:id, :login, :created_at, :email, :website, :bio] } end + # add an extra attribute to private_options + def self.private_options_plus(array) + { :only => User.private_options[:only].push(array).flatten } + end + + # set new api key def set_new_api_key! new_api_key = generate_api_key(16, 'user') @@ -90,4 +131,3 @@ class User < ActiveRecord::Base end - diff --git a/app/views/api_keys/_index.html.erb b/app/views/api_keys/_index.html.erb index 1e1675e..a928cc5 100644 --- a/app/views/api_keys/_index.html.erb +++ b/app/views/api_keys/_index.html.erb @@ -1,11 +1,11 @@

<%= t(:api_key_write) %>

<%= @write_key %> -

+

<%= button_to t(:api_key_write_new), channel_api_keys_path(@channel, :write => 1), :data => { :confirm => t(:confirm_new_api_key) } %> -

+

<%= t(:api_key_read) %>

<% @read_keys.each do |read_key| %> @@ -29,12 +29,12 @@ <%= button_to t(:api_key_delete), channel_api_key_path(@channel, read_key), :method => 'delete', :data => { :confirm => t(:confirm_read_key_delete) } %> -

+

<% end %> <%= button_to t(:api_key_read_new), channel_api_keys_path(@channel, :write => 0) %> -
+