Improve channel, user and api_key models with basic scopes, correct references and protect attributes.

Simplify channels & user controllers and models, moving code to the models.
Remove catch-all route and ensure all actions are specified, fixing url paths.
Change clone to dup in feed_controller as using clone seems to affect the cloned object (problem only in ruby 1.9.3?)
This commit is contained in:
Alan Bradburne 2012-02-09 23:42:16 +00:00
parent bfd5b29a9a
commit 4f0354303f
21 changed files with 198 additions and 172 deletions

View File

@ -1,23 +1,22 @@
class ApiKeysController < ApplicationController class ApiKeysController < ApplicationController
include KeyUtilities
before_filter :require_user, :set_channels_menu before_filter :require_user, :set_channels_menu
def index def index
get_channel_data @channel = current_user.channels.find(params[:channel_id])
@read_keys = ApiKey.find(:all, :conditions => { :channel_id => @channel.id, :user_id => current_user.id, :write_flag => 0 }) @write_key = @channel.api_keys.write_keys.first
@read_keys = @channel.api_keys.read_keys
end end
def destroy def destroy
@api_key = ApiKey.find_by_api_key(params[:api_key]) current_user.api_keys.find_by_api_key(params[:id]).try(:destroy)
@api_key.delete if @api_key.user_id == current_user.id
redirect_to :back redirect_to :back
end end
def create def create
@channel = Channel.find(params[:channel_id]) @channel = current_user.channels.find(params[:channel_id])
# make sure channel belongs to current user @api_key = @channel.api_keys.write_keys.first
check_permissions(@channel)
@api_key = ApiKey.find(:first, :conditions => { :channel_id => @channel.id, :user_id => current_user.id, :write_flag => 1 } )
# if no api key found or read api key # if no api key found or read api key
if (@api_key.nil? or params[:write] == '0') if (@api_key.nil? or params[:write] == '0')
@ -32,14 +31,12 @@ class ApiKeysController < ApplicationController
@api_key.save @api_key.save
# redirect # redirect
redirect_to channel_api_keys_path(@channel.id) and return redirect_to channel_api_keys_path(@channel)
end end
def update def update
@api_key = ApiKey.find_by_api_key(params[:api_key][:api_key]) @api_key = current_user.api_keys.find_by_api_key(params[:id])
@api_key.update_attributes(params[:api_key])
@api_key.note = params[:api_key][:note] redirect_to :back
@api_key.save if current_user.id == @api_key.user_id
redirect_to channel_api_keys_path(@api_key.channel)
end end
end end

View File

@ -94,14 +94,9 @@ class ApplicationController < ActionController::Base
# gets the same data for showing or editing # gets the same data for showing or editing
def get_channel_data def get_channel_data
@channel = Channel.find(params[:channel_id]) if params[:channel_id] @channel = current_user.channels.find(params[:channel_id]) if params[:channel_id]
@channel = Channel.find(params[:id]) if @channel.nil? and params[:id] @channel = current_user.channels.find(params[:id]) if @channel.nil? and params[:id]
@key = '' @key = @channel.api_keys.write_keys.first.try(:api_key) || ""
# make sure channel belongs to current user
check_permissions(@channel)
@api_key = ApiKey.find(:first, :conditions => { :channel_id => @channel.id, :user_id => current_user.id, :write_flag => 1 } )
@key = @api_key.api_key if @api_key
end end
def check_permissions(channel) def check_permissions(channel)
@ -131,18 +126,6 @@ class ApplicationController < ActionController::Base
return feed_unauthorized.to_xml(:only => :entry_id) return feed_unauthorized.to_xml(:only => :entry_id)
end end
# generates a database unique api key
def generate_api_key(size = 16)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
k = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if key exists in database, regenerate key
k = generate_api_key if ApiKey.find_by_api_key(k)
# output the key
return k
end
# options: days = how many days ago, start = start date, end = end date, offset = timezone offset # options: days = how many days ago, start = start date, end = end date, offset = timezone offset
def get_date_range(params) def get_date_range(params)
# set timezone correctly # set timezone correctly

View File

@ -21,72 +21,33 @@ class ChannelsController < ApplicationController
end end
def update def update
@channel = Channel.find(params[:id]) @channel = current_user.channels.find(params[:id])
# make sure channel belongs to current user
check_permissions(@channel)
# protect against bots
render :text => '' and return if params[:userlogin].length > 0
@channel.update_attributes(params[:channel]) @channel.update_attributes(params[:channel])
@channel.name = "#{t(:channel_default_name)} #{@channel.id}" if params[:channel][:name].empty?
@channel.save
redirect_to channel_path(@channel.id) and return redirect_to channel_path(@channel.id)
end end
def create def create
# protect against bots channel = current_user.channels.create(:field1 => "#{t(:channel_default_field)} 1")
render :text => '' and return if params[:userlogin].length > 0 channel.add_write_api_key
# get default name for field
@d = t(:channel_default_field)
# add channel with defaults
@channel = Channel.new(:field1 => "#{@d}1")
@channel.user_id = current_user.id
@channel.save
# now that the channel is saved, we can create the default name
@channel.name = "#{t(:channel_default_name)} #{@channel.id}"
@channel.save
# create an api key for this channel
@api_key = ApiKey.new
@api_key.channel_id = @channel.id
@api_key.user_id = current_user.id
@api_key.write_flag = 1
@api_key.api_key = generate_api_key
@api_key.save
# redirect to edit the newly created channel # redirect to edit the newly created channel
redirect_to edit_channel_path(@channel.id) redirect_to edit_channel_path(channel)
end end
# clear all data from a channel # clear all data from a channel
def clear def clear
channel = Channel.find(params[:id]) channel = current_user.channels.find(params[:id])
# make sure channel belongs to current user channel.feeds.delete_all
check_permissions(channel) channel.update_attribute(:last_entry_id, nil)
# do the delete
channel.feeds.each do |f|
f.delete
end
# set the channel's last_entry_id to nil
channel.last_entry_id = nil
channel.save
redirect_to channels_path redirect_to channels_path
end end
def destroy def destroy
@channel = Channel.find(params[:id]) channel = current_user.channels.find(params[:id])
# make sure channel belongs to current user channel.destroy
check_permissions(@channel)
# do the delete
@channel.delete
redirect_to channels_path redirect_to channels_path
end end
@ -255,6 +216,9 @@ class ChannelsController < ApplicationController
redirect_to channel_path(channel.id) redirect_to channel_path(channel.id)
end end
private
# determine if the date can be parsed # determine if the date can be parsed
def date_parsable?(date) def date_parsable?(date)
return !is_a_number?(date) return !is_a_number?(date)

View File

@ -556,7 +556,7 @@ class FeedController < ApplicationController
# creates an empty clone of an object # creates an empty clone of an object
def create_empty_clone(object) def create_empty_clone(object)
empty_clone = object.clone empty_clone = object.dup
empty_clone.attribute_names.each { |attr| empty_clone[attr] = nil } empty_clone.attribute_names.each { |attr| empty_clone[attr] = nil }
return empty_clone return empty_clone
end end

View File

@ -1,23 +1,20 @@
class MailerController < ApplicationController class MailerController < ApplicationController
def resetpassword def resetpassword
# protect against bots
render :text => '' and return if params[:userlogin].length > 0
@user = User.find_by_login_or_email(params[:user][:login]) @user = User.find_by_login_or_email(params[:user][:login])
if @user.nil? if @user.nil?
sleep 2
session[:mail_message] = t(:account_not_found) session[:mail_message] = t(:account_not_found)
else else
begin begin
@user.reset_perishable_token! @user.reset_perishable_token!
#Mailer.password_reset(@user, "https://www.thingspeak.com/users/reset_password/#{@user.id}?token=#{@user.perishable_token}").deliver # Mailer.password_reset(@user, "https://www.thingspeak.com/users/#{@user.id}/reset_password?token=#{@user.perishable_token}").deliver
session[:mail_message] = t(:password_reset_mailed) session[:mail_message] = t(:password_reset_mailed)
rescue rescue
session[:mail_message] = t(:password_reset_error) session[:mail_message] = t(:password_reset_error)
end end
end end
redirect_to :controller => 'user_session', :action => 'new' redirect_to login_path
end end
end end

View File

@ -8,15 +8,12 @@ class UsersController < ApplicationController
end end
def create def create
# protect against bots
render :text => '' and return if params[:userlogin].length > 0
@user = User.new(params[:user]) @user = User.new(params[:user])
# save user # save user
if @user.valid? if @user.valid?
if @user.save if @user.save
redirect_back_or_default account_path and return redirect_back_or_default account_path
end end
else else
render :action => :new render :action => :new
@ -26,17 +23,16 @@ class UsersController < ApplicationController
def show def show
@menu = 'account' @menu = 'account'
@user = @current_user @user = current_user
end end
def edit def edit
@menu = 'account' @menu = 'account'
@user = @current_user @user = current_user
end end
# displays forgot password page # displays forgot password page
def forgot_password def forgot_password
@user = User.new
end end
# this action is called from an email link when a password reset is requested # this action is called from an email link when a password reset is requested
@ -44,7 +40,7 @@ class UsersController < ApplicationController
# if user has been logged in (due to previous form submission) # if user has been logged in (due to previous form submission)
if !current_user.nil? if !current_user.nil?
@user = current_user @user = current_user
@user.errors.add_to_base(t(:password_problem)) @user.errors.add(t(:password_problem))
@valid_link = true @valid_link = true
else else
@user = User.find_by_id(params[:id]) @user = User.find_by_id(params[:id])
@ -76,13 +72,13 @@ class UsersController < ApplicationController
def update def update
@menu = 'account' @menu = 'account'
@user = @current_user # makes our views "cleaner" and more consistent @user = current_user # makes our views "cleaner" and more consistent
# check current password and update # check current password and update
if @user.valid_password?(params[:password_current]) && @user.update_attributes(params[:user]) if @user.valid_password?(params[:password_current]) && @user.update_attributes(params[:user])
redirect_to account_path redirect_to account_path
else else
@user.errors.add_to_base(t(:password_incorrect)) @user.errors.add :base, t(:password_incorrect)
render :action => :edit render :edit
end end
end end

View File

@ -1,7 +1,22 @@
class ApiKey < ActiveRecord::Base class ApiKey < ActiveRecord::Base
belongs_to :channel belongs_to :channel
belongs_to :user
validates_uniqueness_of :api_key validates_uniqueness_of :api_key
scope :write_keys, :conditions => { :write_flag => true }
scope :read_keys, :conditions => { :write_flag => false }
attr_readonly :created_at
attr_accessible :note
def to_s
api_key
end
def to_param
api_key
end
end end

View File

@ -1,7 +1,40 @@
class Channel < ActiveRecord::Base class Channel < ActiveRecord::Base
include KeyUtilities
belongs_to :user belongs_to :user
has_many :feeds has_many :feeds
has_many :api_keys has_many :api_keys
attr_readonly :created_at
attr_protected :user_id, :last_entry_id
after_create :set_initial_default_name
before_validation :set_default_name
validates :name, :presence => true, :on => :update
def add_write_api_key
write_key = self.api_keys.new
write_key.user = self.user
write_key.write_flag = true
write_key.api_key = generate_api_key
write_key.save
end
def field_label(field_number)
self["field#{field_number}"]
end
private
def set_default_name
self.name = "#{I18n.t(:channel_default_name)} #{self.id}" if self.name.blank?
end
def set_initial_default_name
update_attribute(:name, "#{I18n.t(:channel_default_name)} #{self.id}")
end
end end

View File

@ -2,6 +2,9 @@ class Feed < ActiveRecord::Base
belongs_to :channel belongs_to :channel
self.include_root_in_json = false self.include_root_in_json = false
attr_readonly :created_at
attr_protected :channel_id
end end

View File

@ -1,5 +1,6 @@
class User < ActiveRecord::Base class User < ActiveRecord::Base
has_many :channels has_many :channels
has_many :api_keys
acts_as_authentic acts_as_authentic

View File

@ -5,7 +5,7 @@
</h2> </h2>
<h3><%= t(:api_key_write) %></h3> <h3><%= t(:api_key_write) %></h3>
<%= @key %> <%= @write_key %>
<br /><br /> <br /><br />
@ -18,21 +18,21 @@
<table> <table>
<tr> <tr>
<td><%= t(:api_key_key) %>:</td> <td><%= t(:api_key_key) %>:</td>
<td><%= read_key.api_key %></td> <td><%= read_key %></td>
</tr> </tr>
<tr> <tr>
<td class="VAT"><%= t(:note) %>:</td> <td class="VAT"><%= t(:note) %>:</td>
<td> <td>
<%= form_for read_key, :as => :api_key, :url => { :controller => 'api_keys', :action => 'update' }, :html => {:method => 'put'} do |f| %> <%= form_for read_key, :as => :api_key, :url => channel_api_key_path(@channel, read_key), :html => {:method => 'put'} do |f| %>
<%= f.text_area :note, :cols => 30, :rows => 4 %> <%= f.text_area :note, :cols => 30, :rows => 4 %>
</td> </td>
</tr> </tr>
<tr> <tr>
<td><%= f.hidden_field :api_key, :value => read_key.api_key %></td> <td></td>
<td> <td>
<div class="FL"><%= f.submit t(:note_save) %></div> <div class="FL"><%= f.submit t(:note_save) %></div>
<% end %> <% end %>
<%= button_to t(:api_key_delete), { :controller => 'api_keys', :action => 'destroy', :api_key => read_key.api_key}, :confirm => t(:confirm_read_key_delete) %></td> <%= button_to t(:api_key_delete), channel_api_key_path(@channel, read_key) , :method => 'delete', :confirm => t(:confirm_read_key_delete) %></td>
</tr> </tr>
</table> </table>
<br /><br /> <br /><br />

View File

@ -78,12 +78,12 @@
<br /><br /> <br /><br />
<h2><%= t(:channel_clear_message) %></h2> <h2><%= t(:channel_clear_message) %></h2>
<%= button_to t(:channel_clear), { :controller => 'channels', :action => 'clear', :id => @channel.id }, :confirm => t(:confirm_channel_clear) %> <%= button_to t(:channel_clear), clear_channel_path(@channel), :confirm => t(:confirm_channel_clear) %>
<br /><br /> <br /><br />
<h2><%= t(:channel_delete_message) %></h2> <h2><%= t(:channel_delete_message) %></h2>
<%= button_to t(:channel_delete), channel_path(@channel.id), :method => 'delete', :confirm => t(:confirm_channel_delete) %> <%= button_to t(:channel_delete), channel_path(@channel), :method => 'delete', :confirm => t(:confirm_channel_delete) %>
<script type="text/javascript"> <script type="text/javascript">
// remember default field label // remember default field label

View File

@ -6,7 +6,7 @@
<%= t(:upload_select) %> <%= t(:upload_select) %>
<br /><br /> <br /><br />
<%= form_for :upload, :url => {:controller => 'channels', :action => 'upload', :channel_id => params[:channel_id]}, :html => { :multipart => true } do |f| %> <%= form_for :upload, :url => upload_channel_path(@channel), :html => { :multipart => true } do |f| %>
<%= f.file_field :csv %> <%= f.file_field :csv %>
<br /><br /> <br /><br />
<%= t(:time_zone) %> <%= t(:time_zone) %>

View File

@ -1,4 +1,5 @@
<%= javascript_include_tag 'rest' %> <script type="text/javascript" src="/javascripts/rest.js"></script>
<h2> <h2>
<%= link_to t(:channels), channels_path %> &raquo; <%= link_to t(:channels), channels_path %> &raquo;

View File

@ -1,6 +1,6 @@
<div id="options"> <div id="options">
<% if current_user %> <% if current_user %>
<span class="action"> <%= link_to t(:signout), logout_path %></span> <span class="action"> <%= link_to t(:signout), logout_path, :method => 'delete' %></span>
<% else %> <% else %>
<span class="action"> <span class="action">
<%= link_to t(:signup), new_user_path %> <%= link_to t(:signup), new_user_path %>

View File

@ -1,4 +1,4 @@
<%= form_for (@user_session = UserSession.new), :url => user_session_path, :html => { :id => 'loginform' } do |f| %> <%= form_for @user_session, :url => user_session_path, :html => { :id => 'loginform' } do |f| %>
<input name='userlogin' class='userlogin' /> <input name='userlogin' class='userlogin' />
<%= f.hidden_field :remember_me, :value => false %> <%= f.hidden_field :remember_me, :value => false %>
<table id="login" class="round"> <table id="login" class="round">
@ -21,7 +21,7 @@
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td><%= link_to t(:forgot), forgot_password_path, :id => 'forgot_password' %></td> <td><%= link_to t(:forgot), forgot_password_users_path, :id => 'forgot_password' %></td>
</tr> </tr>
<tr> <tr>
<td></td> <td></td>

View File

@ -1,7 +1,7 @@
<h2><%= t(:password_forgot) %></h2> <h2><%= t(:password_forgot) %></h2>
<%= t(:password_forgot_message) %> <%= t(:password_forgot_message) %>
<br /><br /> <br /><br />
<%= form_for @user, :url => { :controller => 'mailer', :action => 'resetpassword' } do |f| %> <%= form_for :user, :url => resetpassword_path do |f| %>
<input name='userlogin' class='userlogin' /> <input name='userlogin' class='userlogin' />
<%= f.text_field :login %> <%= f.text_field :login %>
<%= f.submit t(:submit) %> <%= f.submit t(:submit) %>

View File

@ -1,6 +1,6 @@
<% if @valid_link %> <% if @valid_link %>
<h2><%= t(:password_new) %></h2> <h2><%= t(:password_new) %></h2>
<%= form_for @user, :url => { :controller => 'users', :action => 'change_password', :id => @user.id } do |f| %> <%= form_for @user, :url => change_password_user_path(@user) do |f| %>
<%= error_messages_for 'user', :header_message => t(:try_again), :message => t(:password_new_error) %> <%= error_messages_for 'user', :header_message => t(:try_again), :message => t(:password_new_error) %>
<input name='userlogin' class='userlogin' /> <input name='userlogin' class='userlogin' />
<table class="bigtable"> <table class="bigtable">

View File

@ -0,0 +1,12 @@
# This file contains settings for ActionController::ParamsWrapper which
# is enabled by default.
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json]
end
# # Disable root element in JSON by default.
# ActiveSupport.on_load(:active_record) do
# self.include_root_in_json = false
# end

View File

@ -11,7 +11,15 @@ Thingspeak::Application.routes.draw do
resource :user_session resource :user_session
resource 'account', :to => 'users' resource 'account', :to => 'users'
resources :users resources :users do
member do
get :reset_password
put :change_password
end
collection do
get :forgot_password
end
end
# specific feeds # specific feeds
match 'channels/:channel_id/feed(s)(.:format)' => 'feed#index' match 'channels/:channel_id/feed(s)(.:format)' => 'feed#index'
@ -25,18 +33,20 @@ Thingspeak::Application.routes.draw do
# nest feeds into channels # nest feeds into channels
resources :channels do resources :channels do
member do
get :import
post :upload
post :clear
end
resources :feed resources :feed
resources :feeds, :to => 'feed' resources :feeds, :to => 'feed'
resources :api_keys resources :api_keys, :except => [:show, :edit]
resources :status resources :status
resources :statuses, :to => 'statuses' resources :statuses, :to => 'statuses'
resources :charts resources :charts
end end
match 'login' => 'user_sessions#new', :as => :login match 'login' => 'user_sessions#new', :as => :login, :via => :get
match 'logout' => 'user_sessions#destroy', :as => :logout match 'logout' => 'user_sessions#destroy', :as => :logout, :via => :delete
match 'users/reset_password', :to => 'users#reset_password', :as => 'reset_password' match 'mailer/resetpassword', :to => 'mailer#resetpassword', :as => :resetpassword, :via => :post
match 'forgot_password', :to => 'users#forgot_password', :as => 'forgot_password'
match ':controller(/:action(/:id(.:format)))'
end end

14
lib/key_utilities.rb Normal file
View File

@ -0,0 +1,14 @@
module KeyUtilities
# generates a database unique api key
def generate_api_key(size = 16)
alphanumerics = ('0'..'9').to_a + ('A'..'Z').to_a
k = (0..size).map {alphanumerics[Kernel.rand(36)]}.join
# if key exists in database, regenerate key
k = generate_api_key if ApiKey.find_by_api_key(k)
# output the key
k
end
end