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