diff options
79 files changed, 1432 insertions, 660 deletions
@@ -13,9 +13,11 @@ # Ignore all logfiles and tempfiles. /log/*.log /tmp +*~ /pkg /*/pkg /log +Gemfile.lock */Gemfile.lock test/dummy/log/* test/dummy/tmp/* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..984e24a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,2 @@ +services: + - couchdb @@ -9,7 +9,8 @@ eval(File.read(File.dirname(__FILE__) + '/ui_dependencies.rb')) gem "leap_web_core", :path => 'core' gem 'leap_web_users', :path => 'users' gem 'leap_web_certs', :path => 'certs' -# gem 'leap_web_help', :path => 'help' +gem 'leap_web_help', :path => 'help' # To use debugger -gem 'ruby-debug' +gem 'debugger', :platforms => :mri_19 +gem 'ruby-debug', :platforms => :mri_18 diff --git a/Gemfile.lock b/Gemfile.lock deleted file mode 100644 index fe49476..0000000 --- a/Gemfile.lock +++ /dev/null @@ -1,182 +0,0 @@ -PATH - remote: certs - specs: - leap_web_certs (0.1.0) - leap_web_core (= 0.1.0) - -PATH - remote: core - specs: - leap_web_core (0.1.0) - couchrest (~> 1.1.3) - couchrest_model (~> 2.0.0.beta2) - couchrest_session_store (~> 0.0.1) - json - rails (~> 3.2.8) - -PATH - remote: users - specs: - leap_web_users (0.1.0) - leap_web_core (= 0.1.0) - ruby-srp (~> 0.1.1) - -GEM - remote: https://rubygems.org/ - remote: http://rubygems.org/ - specs: - actionmailer (3.2.8) - actionpack (= 3.2.8) - mail (~> 2.4.4) - actionpack (3.2.8) - activemodel (= 3.2.8) - activesupport (= 3.2.8) - builder (~> 3.0.0) - erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.0) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.1.3) - activemodel (3.2.8) - activesupport (= 3.2.8) - builder (~> 3.0.0) - activerecord (3.2.8) - activemodel (= 3.2.8) - activesupport (= 3.2.8) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.8) - activemodel (= 3.2.8) - activesupport (= 3.2.8) - activesupport (3.2.8) - i18n (~> 0.6) - multi_json (~> 1.0) - arel (3.0.2) - bootstrap-sass (2.0.4.2) - builder (3.0.3) - coffee-rails (3.2.2) - coffee-script (>= 2.2.0) - railties (~> 3.2.0) - coffee-script (2.2.0) - coffee-script-source - execjs - coffee-script-source (1.3.3) - columnize (0.3.6) - couchrest (1.1.3) - mime-types (~> 1.15) - multi_json (~> 1.0) - rest-client (~> 1.6.1) - couchrest_model (2.0.0.beta2) - activemodel (~> 3.0) - couchrest (~> 1.1.3) - mime-types (~> 1.15) - tzinfo (~> 0.3.22) - couchrest_session_store (0.0.1) - couchrest - couchrest_model - erubis (2.7.0) - execjs (1.4.0) - multi_json (~> 1.0) - haml (3.1.7) - haml-rails (0.3.5) - actionpack (>= 3.1, < 4.1) - activesupport (>= 3.1, < 4.1) - haml (~> 3.1) - railties (>= 3.1, < 4.1) - hike (1.2.1) - i18n (0.6.1) - journey (1.0.4) - jquery-rails (2.1.3) - railties (>= 3.1.0, < 5.0) - thor (~> 0.14) - json (1.7.5) - libv8 (3.3.10.4) - linecache (0.46) - rbx-require-relative (> 0.0.4) - mail (2.4.4) - i18n (>= 0.4.0) - mime-types (~> 1.16) - treetop (~> 1.4.8) - metaclass (0.0.1) - mime-types (1.19) - mocha (0.12.6) - metaclass (~> 0.0.1) - multi_json (1.3.6) - polyglot (0.3.3) - rack (1.4.1) - rack-cache (1.2) - rack (>= 0.4) - rack-ssl (1.3.2) - rack - rack-test (0.6.2) - rack (>= 1.0) - rails (3.2.8) - actionmailer (= 3.2.8) - actionpack (= 3.2.8) - activerecord (= 3.2.8) - activeresource (= 3.2.8) - activesupport (= 3.2.8) - bundler (~> 1.0) - railties (= 3.2.8) - railties (3.2.8) - actionpack (= 3.2.8) - activesupport (= 3.2.8) - rack-ssl (~> 1.3.2) - rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (0.9.2.2) - rbx-require-relative (0.0.9) - rdoc (3.12) - json (~> 1.4) - rest-client (1.6.7) - mime-types (>= 1.16) - ruby-debug (0.10.4) - columnize (>= 0.1) - ruby-debug-base (~> 0.10.4.0) - ruby-debug-base (0.10.4) - linecache (>= 0.3) - ruby-srp (0.1.1) - sass (3.2.1) - sass-rails (3.2.5) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) - simple_form (2.0.4) - actionpack (~> 3.0) - activemodel (~> 3.0) - sprockets (2.1.3) - hike (~> 1.2) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - therubyracer (0.10.2) - libv8 (~> 3.3.10) - thor (0.16.0) - tilt (1.3.3) - treetop (1.4.10) - polyglot - polyglot (>= 0.3.1) - tzinfo (0.3.33) - uglifier (1.2.7) - execjs (>= 0.3.0) - multi_json (~> 1.3) - -PLATFORMS - ruby - -DEPENDENCIES - bootstrap-sass (~> 2.0.4) - coffee-rails (~> 3.2.2) - haml (~> 3.1.7) - haml-rails (~> 0.3.4) - jquery-rails - leap_web_certs! - leap_web_core! - leap_web_users! - mocha - ruby-debug - sass-rails (~> 3.2.5) - simple_form - therubyracer - uglifier (~> 1.2.7) @@ -6,11 +6,11 @@ The webapp only depends on very basic ruby packages and installs the other requi ### Packages ### -For now we are using ruby 1.8.7. The following packages need to be installed: +The following packages need to be installed: * git -* ruby1.8 -* rubygems1.8 +* ruby (1.8.7 and 1.9.3 work) +* rubygems * couchdb ### Gems ### diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e77d82 --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +LEAP Web +--------------------- + +"LEAP Web" is the web-based component of the LEAP Platform, providing the following services: + +* REST API for user registration. +* Admin interface to manage users. +* Client certificate distribution and renewal. +* User support help tickets. + +This web application is written in Ruby on Rails 3, using CouchDB as the backend data store. + +Original code specific to this web application is licensed under the GNU Affero General Public License (version 3.0 or higher). See http://www.gnu.org/licenses/agpl-3.0.html for more information. + +Documentation +--------------------------- + +For more information, see these files in the ``doc`` directory: + +* DEPLOY -- for notes on deployment. +* DEVELOP -- for developer notes. +* CUSTOM -- how to customize. + +Installation +--------------------------- + +Typically, this application is installed automatically as part of the LEAP Platform. To install it manually for testing or development, follow these instructions: + +### Install system requirements + + sudo apt-get install git ruby1.8 rubygems1.8 couchdb + sudo gem install bundler + +On Debian Wheezy or later, there is a Debian package for bundler, so you can alternately run ``sudo apt-get install bundler``. + +### Download source + + git clone git://leap.se/leap_web + cd leap_web + git submodule update --init + +### Install required ruby libraries + + cd leap_web + bundle + +Typically, you run ``bundle`` as a normal user and it will ask you for a sudo password when it is time to install the required gems. If you don't have sudo, run ``bundle`` as root. + +Configuration +---------------------------- + +The webapp can hand out certs for the EIP client. These certs are either picked from a pool in CouchDB or from a file. For now you can either run [Leap CA](http://github.com/leapcode/leap_ca) to fill the pool or you can put your certs file in config/cert. + +We also ship provider information through the webapp. For now please add your eip-service.json to the public/config directory. + +Copy the example configuration file and customize as appropriate: + cp config/config.yml.example config/config.yml + +Running +----------------------------- + + cd leap_web + rails server + +Then open http://localhost:3000 in your web browser. + +To peruse the database, visit http://localhost:5984/_utils/ + diff --git a/Readme.md b/Readme.md deleted file mode 100644 index 8b51b4d..0000000 --- a/Readme.md +++ /dev/null @@ -1,24 +0,0 @@ -# Leap Web # - -Web application for LEAP. Currently Leap Web allows Leap providers to manage users, hand out certs for the EIP. - -## Functions ## - -### Supported ### - -* *User Management* - User Registration and Authentication -* *Cert Distribution* - Certs for the Encrypted Internet Proxy - -### Under Development ### - -* *Help Desk* - Managing Help Requests - - -## Documentation ## - -* [INSTALL](https://github.com/leapcode/leap_web/blob/master/INSTALL.md) for installation instructions -* [DEPLOY](https://github.com/leapcode/leap_web/blob/master/DEPLOY.md) for deployment -* [DEVELOP](https://github.com/leapcode/leap_web/blob/master/DEVELOP.md) for developer notes. -* [CUSTOM](https://github.com/leapcode/leap_web/blob/master/CUSTOM.md) to customize. - - diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index a0b89db..f7ca1ec 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -15,3 +15,6 @@ //= require srp //= require users //= require_tree . +//= require bootstrap +//= require rails.validations +//= require rails.validations.simple_form diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index cbd46a7..8dec07d 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1 +1,14 @@ @import "bootstrap"; +body { + padding: 40px; +} +@import "bootstrap-responsive"; + + +table.table-hover .btn { + opacity: 0; +} + +table.table-hover tr:hover .btn { + opacity: 1; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e8065d9..be7aa1f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ class ApplicationController < ActionController::Base protect_from_forgery + + ActiveSupport.run_load_hooks(:application_controller, self) end diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 11f18de..9e68674 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,4 +1,11 @@ -%h1 Leap Web Demo Try to fetch a = link_to "cert", cert_path + +%p +Try to create a += link_to "ticket", new_ticket_path + +%p +See all += link_to "tickets", tickets_path diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml new file mode 100644 index 0000000..b75eed7 --- /dev/null +++ b/app/views/layouts/_navigation.html.haml @@ -0,0 +1,6 @@ += link_to "Leap Web", root_path, :class => 'brand' +%ul.nav + // = render '/tickets/nav' + +%ul.nav.pull-right + = render '/sessions/nav' diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb deleted file mode 100644 index ce68ec8..0000000 --- a/app/views/layouts/application.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>LeapWeb</title> - <%= stylesheet_link_tag "application", :media => "all" %> - <%= javascript_include_tag "application" %> - <%= csrf_meta_tags %> -</head> -<body> - -<%= yield %> - -</body> -</html> diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml new file mode 100644 index 0000000..a57d65e --- /dev/null +++ b/app/views/layouts/application.html.haml @@ -0,0 +1,23 @@ +!!! +%html + %head + %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"} + %title= content_for?(:title) ? yield(:title) : "Leap Web" + %meta{:content => content_for?(:description) ? yield(:description) : "Leap Web", :name => "description"} + = stylesheet_link_tag "application", :media => "all" + = javascript_include_tag "application" + = csrf_meta_tags + = yield(:head) + %body + %header.navbar.navbar-fixed-top + %nav.navbar-inner + .container + = render 'layouts/navigation' + #main{:role => "main"} + .container + .content + .row + .span12 + //= render 'layouts/messages' + = yield + %footer diff --git a/certs/app/controllers/certs_controller.rb b/certs/app/controllers/certs_controller.rb index 6988a38..402bef3 100644 --- a/certs/app/controllers/certs_controller.rb +++ b/certs/app/controllers/certs_controller.rb @@ -1,5 +1,7 @@ class CertsController < ApplicationController + before_filter :authorize + # GET /cert def show @cert = Cert.pick_from_pool diff --git a/certs/test/functional/certs_controller_test.rb b/certs/test/functional/certs_controller_test.rb index 295515b..9bba8c0 100644 --- a/certs/test/functional/certs_controller_test.rb +++ b/certs/test/functional/certs_controller_test.rb @@ -4,7 +4,14 @@ class CertsControllerTest < ActionController::TestCase setup do end + test "should require login" do + get :show + assert_response :redirect + assert_redirected_to login_url + end + test "should send cert" do + login cert = stub :zipped => "adsf", :zipname => "cert_stub.zip" Cert.expects(:pick_from_pool).returns(cert) get :show diff --git a/certs/test/test_helper.rb b/certs/test/test_helper.rb index 08d4d41..f6b4eb8 100644 --- a/certs/test/test_helper.rb +++ b/certs/test/test_helper.rb @@ -1,7 +1,7 @@ ENV["RAILS_ENV"] = "test" require File.expand_path('../../../test/dummy/config/environment', __FILE__) require 'rails/test_help' -require 'mocha' +require 'mocha/setup' Rails.backtrace_cleaner.remove_silencers! diff --git a/common_dependencies.rb b/common_dependencies.rb index 5312a80..a6691cf 100644 --- a/common_dependencies.rb +++ b/common_dependencies.rb @@ -1,6 +1,6 @@ source "http://rubygems.org" group :test do - gem 'mocha', :require => false + gem 'mocha', '~> 0.13.0', :require => false end diff --git a/config/config.yml b/config/config.yml new file mode 100644 index 0000000..c34dd10 --- /dev/null +++ b/config/config.yml @@ -0,0 +1,8 @@ +development: + admins: [admin, admin2] + +test: + admins: [admin, admin2] + +production: + admins: [] diff --git a/config/config.yml.example b/config/config.yml.example new file mode 100644 index 0000000..c34dd10 --- /dev/null +++ b/config/config.yml.example @@ -0,0 +1,8 @@ +development: + admins: [admin, admin2] + +test: + admins: [admin, admin2] + +production: + admins: [] diff --git a/config/deploy.rb b/config/deploy.rb deleted file mode 100644 index 9dc058a..0000000 --- a/config/deploy.rb +++ /dev/null @@ -1,37 +0,0 @@ -require "bundler/capistrano" - -set :application, "webapp" - -set :scm, :git -set :repository, "git://leap.se/leap_web" -set :branch, "deploy" - -set :deploy_via, :remote_cache -set :deploy_to, '/home/webapp' -set :use_sudo, false - -set :normalize_asset_timestamps, false - -set :user, "webapp" - -set :git_enable_submodules, 1 # we're using an srp js submodule for now - -role :web, "94.103.43.3" # Your HTTP server, Apache/etc -role :app, "94.103.43.3" # This may be the same as your `Web` server -# role :db, "your primary db-server here", :primary => true # This is where Rails migrations will run -# role :db, "your slave db-server here" - -# if you want to clean up old releases on each deploy uncomment this: -# after "deploy:restart", "deploy:cleanup" - -# if you're still using the script/reaper helper you will need -# these http://github.com/rails/irs_process_scripts - -# If you are using Passenger mod_rails uncomment this: -# namespace :deploy do -# task :start do ; end -# task :stop do ; end -# task :restart, :roles => :app, :except => { :no_release => true } do -# run "#{try_sudo} touch #{File.join(current_path,'tmp','restart.txt')}" -# end -# end diff --git a/config/initializers/client_side_validations.rb b/config/initializers/client_side_validations.rb new file mode 100644 index 0000000..252aded --- /dev/null +++ b/config/initializers/client_side_validations.rb @@ -0,0 +1,14 @@ +# ClientSideValidations Initializer + +# Uncomment to disable uniqueness validator, possible security issue +ClientSideValidations::Config.disabled_validators = [:uniqueness] + +# Uncomment the following block if you want each input field to have the validation messages attached. +ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| + unless html_tag =~ /^<label/ + %{<div class="field_with_errors">#{html_tag}<label for="#{instance.send(:tag_id)}" class="message">#{instance.error_message.first}</label></div>}.html_safe + else + %{<div class="field_with_errors">#{html_tag}</div>}.html_safe + end +end + diff --git a/config/initializers/load_config.rb b/config/initializers/load_config.rb new file mode 100644 index 0000000..e687429 --- /dev/null +++ b/config/initializers/load_config.rb @@ -0,0 +1 @@ +APP_CONFIG = YAML.load_file("#{Rails.root}/config/config.yml")[Rails.env] diff --git a/core/config/initializers/simple_form.rb b/core/config/initializers/simple_form.rb index b346dfa..e3f8d09 100644 --- a/core/config/initializers/simple_form.rb +++ b/core/config/initializers/simple_form.rb @@ -1,181 +1,142 @@ # Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + # Wrappers are used by the form builder to generate a + # complete input. You can remove any component from the + # wrapper, change the order or even add your own to the + # stack. The options given below are used to wrap the + # whole input. + config.wrappers :default, :class => :input, + :hint_class => :field_with_hint, :error_class => :field_with_errors do |b| + ## Extensions enabled by default + # Any of these extensions can be disabled for a + # given input by passing: `f.input EXTENSION_NAME => false`. + # You can make any of these extensions optional by + # renaming `b.use` to `b.optional`. + + # Determines whether to use HTML5 (:email, :url, ...) + # and required attributes + b.use :html5 + + # Calculates placeholders automatically from I18n + # You can also pass a string as f.input :placeholder => "Placeholder" + b.use :placeholder + + ## Optional extensions + # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup` + # to the input. If so, they will retrieve the values from the model + # if any exists. If you want to enable the lookup for any of those + # extensions by default, you can change `b.optional` to `b.use`. + + # Calculates maxlength from length validations for string inputs + b.optional :maxlength + + # Calculates pattern from format validations for string inputs + b.optional :pattern + + # Calculates min and max from length validations for numeric inputs + b.optional :min_max + + # Calculates readonly automatically from readonly attributes + b.optional :readonly + + ## Inputs + b.use :label_input + b.use :hint, :wrap_with => { :tag => :span, :class => :hint } + b.use :error, :wrap_with => { :tag => :span, :class => :error } + end -if defined? SimpleForm - SimpleForm.setup do |config| - # Wrappers are used by the form builder to generate a - # complete input. You can remove any component from the - # wrapper, change the order or even add your own to the - # stack. The options given below are used to wrap the - # whole input. - config.wrappers :default, :class => :input, - :hint_class => :field_with_hint, :error_class => :field_with_errors do |b| - ## Extensions enabled by default - # Any of these extensions can be disabled for a - # given input by passing: `f.input EXTENSION_NAME => false`. - # You can make any of these extensions optional by - # renaming `b.use` to `b.optional`. - - # Determines whether to use HTML5 (:email, :url, ...) - # and required attributes - b.use :html5 - - # Calculates placeholders automatically from I18n - # You can also pass a string as f.input :placeholder => "Placeholder" - b.use :placeholder - - ## Optional extensions - # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup` - # to the input. If so, they will retrieve the values from the model - # if any exists. If you want to enable the lookup for any of those - # extensions by default, you can change `b.optional` to `b.use`. - - # Calculates maxlength from length validations for string inputs - b.optional :maxlength - - # Calculates pattern from format validations for string inputs - b.optional :pattern - - # Calculates min and max from length validations for numeric inputs - b.optional :min_max - - # Calculates readonly automatically from readonly attributes - b.optional :readonly - - ## Inputs - b.use :label_input - b.use :hint, :wrap_with => { :tag => :span, :class => :hint } - b.use :error, :wrap_with => { :tag => :span, :class => :error } - end - - config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' do |ba| - ba.use :input - ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } - end - end - - config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' do |input| - input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend| - prepend.use :input - end - input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } - input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - end - end - - config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| - b.use :html5 - b.use :placeholder - b.use :label - b.wrapper :tag => 'div', :class => 'controls' do |input| - input.wrapper :tag => 'div', :class => 'input-append' do |append| - append.use :input - end - input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } - input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } - end - end - - # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. - # Check the Bootstrap docs (http://twitter.github.com/bootstrap) - # to learn about the different styles for forms and inputs, - # buttons and other elements. - config.default_wrapper = :bootstrap - - # Define the way to render check boxes / radio buttons with labels. - # Defaults to :nested for bootstrap config. - # :inline => input + label - # :nested => label > input - config.boolean_style = :nested - - # Default class for buttons - config.button_class = 'btn' - - # Method used to tidy up errors. Specify any Rails Array method. - # :first lists the first message for each field. - # Use :to_sentence to list all errors for each field. - # config.error_method = :first - - # Default tag used for error notification helper. - config.error_notification_tag = :div - - # CSS class to add for error notification helper. - config.error_notification_class = 'alert alert-error' - - # ID to add for error notification helper. - # config.error_notification_id = nil - - # Series of attempts to detect a default label method for collection. - # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] - - # Series of attempts to detect a default value method for collection. - # config.collection_value_methods = [ :id, :to_s ] - - # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. - # config.collection_wrapper_tag = nil - - # You can define the class to use on all collection wrappers. Defaulting to none. - # config.collection_wrapper_class = nil - - # You can wrap each item in a collection of radio/check boxes with a tag, - # defaulting to :span. Please note that when using :boolean_style = :nested, - # SimpleForm will force this option to be a label. - # config.item_wrapper_tag = :span - - # You can define a class to use in all item wrappers. Defaulting to none. - # config.item_wrapper_class = nil - - # How the label text should be generated altogether with the required text. - # config.label_text = lambda { |label, required| "#{required} #{label}" } - - # You can define the class to use on all labels. Default is nil. - config.label_class = 'control-label' - - # You can define the class to use on all forms. Default is simple_form. - config.form_class = 'form-horizontal' - - # You can define which elements should obtain additional classes - # config.generate_additional_classes_for = [:wrapper, :label, :input] - - # Whether attributes are required by default (or not). Default is true. - # config.required_by_default = true - - # Tell browsers whether to use default HTML5 validations (novalidate option). - # Default is enabled. - config.browser_validations = true - - # Collection of methods to detect if a file type was given. - # config.file_methods = [ :mounted_as, :file?, :public_filename ] - - # Custom mappings for input types. This should be a hash containing a regexp - # to match as key, and the input type that will be used when the field name - # matches the regexp as value. - # config.input_mappings = { /count/ => :integer } - - # Default priority for time_zone inputs. - # config.time_zone_priority = nil + # The default wrapper to be used by the FormBuilder. + config.default_wrapper = :default - # Default priority for country inputs. - # config.country_priority = nil + # Define the way to render check boxes / radio buttons with labels. + # Defaults to :nested for bootstrap config. + # :inline => input + label + # :nested => label > input + config.boolean_style = :nested - # Default size for text inputs. - # config.default_input_size = 50 + # Default class for buttons + config.button_class = 'btn' - # When false, do not use translations for labels. - # config.translate_labels = true + # Method used to tidy up errors. Specify any Rails Array method. + # :first lists the first message for each field. + # Use :to_sentence to list all errors for each field. + # config.error_method = :first - # Automatically discover new inputs in Rails' autoload path. - # config.inputs_discovery = true + # Default tag used for error notification helper. + config.error_notification_tag = :div - # Cache SimpleForm inputs discovery - # config.cache_discovery = !Rails.env.development? - end + # CSS class to add for error notification helper. + config.error_notification_class = 'alert alert-error' + + # ID to add for error notification helper. + # config.error_notification_id = nil + + # Series of attempts to detect a default label method for collection. + # config.collection_label_methods = [ :to_label, :name, :title, :to_s ] + + # Series of attempts to detect a default value method for collection. + # config.collection_value_methods = [ :id, :to_s ] + + # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none. + # config.collection_wrapper_tag = nil + + # You can define the class to use on all collection wrappers. Defaulting to none. + # config.collection_wrapper_class = nil + + # You can wrap each item in a collection of radio/check boxes with a tag, + # defaulting to :span. Please note that when using :boolean_style = :nested, + # SimpleForm will force this option to be a label. + # config.item_wrapper_tag = :span + + # You can define a class to use in all item wrappers. Defaulting to none. + # config.item_wrapper_class = nil + + # How the label text should be generated altogether with the required text. + # config.label_text = lambda { |label, required| "#{required} #{label}" } + + # You can define the class to use on all labels. Default is nil. + config.label_class = 'control-label' + + # You can define the class to use on all forms. Default is simple_form. + # config.form_class = :simple_form + + # You can define which elements should obtain additional classes + # config.generate_additional_classes_for = [:wrapper, :label, :input] + + # Whether attributes are required by default (or not). Default is true. + # config.required_by_default = true + + # Tell browsers whether to use default HTML5 validations (novalidate option). + # Default is enabled. + config.browser_validations = false + + # Collection of methods to detect if a file type was given. + # config.file_methods = [ :mounted_as, :file?, :public_filename ] + + # Custom mappings for input types. This should be a hash containing a regexp + # to match as key, and the input type that will be used when the field name + # matches the regexp as value. + # config.input_mappings = { /count/ => :integer } + + # Custom wrappers for input types. This should be a hash containing an input + # type as key and the wrapper that will be used for all inputs with specified type. + # config.wrapper_mappings = { :string => :prepend } + + # Default priority for time_zone inputs. + # config.time_zone_priority = nil + + # Default priority for country inputs. + # config.country_priority = nil + + # Default size for text inputs. + # config.default_input_size = 50 + + # When false, do not use translations for labels. + # config.translate_labels = true + + # Automatically discover new inputs in Rails' autoload path. + # config.inputs_discovery = true + + # Cache SimpleForm inputs discovery + # config.cache_discovery = !Rails.env.development? end diff --git a/core/config/initializers/simple_form_bootstrap.rb b/core/config/initializers/simple_form_bootstrap.rb new file mode 100644 index 0000000..1a22967 --- /dev/null +++ b/core/config/initializers/simple_form_bootstrap.rb @@ -0,0 +1,45 @@ +# Use this setup block to configure all options available in SimpleForm. +SimpleForm.setup do |config| + config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |ba| + ba.use :input + ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' } + end + end + + config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend| + prepend.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b| + b.use :html5 + b.use :placeholder + b.use :label + b.wrapper :tag => 'div', :class => 'controls' do |input| + input.wrapper :tag => 'div', :class => 'input-append' do |append| + append.use :input + end + input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' } + input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' } + end + end + + # Wrappers for forms and inputs using the Twitter Bootstrap toolkit. + # Check the Bootstrap docs (http://twitter.github.com/bootstrap) + # to learn about the different styles for forms and inputs, + # buttons and other elements. + config.default_wrapper = :bootstrap +end diff --git a/core/config/locales/simple_form.en.yml b/core/config/locales/simple_form.en.yml new file mode 100644 index 0000000..0df11fe --- /dev/null +++ b/core/config/locales/simple_form.en.yml @@ -0,0 +1,26 @@ +en: + simple_form: + "yes": 'Yes' + "no": 'No' + required: + text: 'required' + mark: '*' + # You can uncomment the line below if you need to overwrite the whole required html. + # When using html, text and mark won't be used. + # html: '<abbr title="required">*</abbr>' + error_notification: + default_message: "Please review the problems below:" + # Labels and hints examples + # labels: + # defaults: + # password: 'Password' + # user: + # new: + # email: 'E-mail to sign in.' + # edit: + # email: 'E-mail.' + # hints: + # defaults: + # username: 'User name to sign in.' + # password: 'No special characters, please.' + diff --git a/core/lib/extensions/testing.rb b/core/lib/extensions/testing.rb index 14a5698..925c023 100644 --- a/core/lib/extensions/testing.rb +++ b/core/lib/extensions/testing.rb @@ -1,17 +1,32 @@ module LeapWebCore module AssertResponses + # response that works with different TestCases: + # ActionController::TestCase has @response + # ActionDispatch::IntegrationTest has @response + # Rack::Test::Methods defines last_response + def get_response + @response || last_response + end + def assert_attachement_filename(name) assert_equal %Q(attachment; filename="#{name}"), - @response.headers["Content-Disposition"] + get_response.headers["Content-Disposition"] end - def assert_json_response(object) - object.stringify_keys! if object.respond_to? :stringify_keys! - assert_equal object, JSON.parse(@response.body) + if object.is_a? Hash + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_equal object, JSON.parse(get_response.body) + else + assert_equal object.to_json, get_response.body + end end + def assert_json_error(object) + object.stringify_keys! if object.respond_to? :stringify_keys! + assert_json_response :errors => object + end end end diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb new file mode 100644 index 0000000..b5f3a63 --- /dev/null +++ b/help/app/controllers/tickets_controller.rb @@ -0,0 +1,73 @@ +class TicketsController < ApplicationController + + respond_to :html #, :json + #has_scope :open, :type => boolean + + def new + @ticket = Ticket.new + @ticket.comments.build + end + + def create + @ticket = Ticket.new(params[:ticket]) + if current_user + @ticket.created_by = current_user.id + @ticket.email = current_user.email if current_user.email + @ticket.comments.last.posted_by = current_user.id + else + @ticket.comments.last.posted_by = nil #hacky, but protecting this attribute doesn't work right, so this should make sure it isn't set. + end + + flash[:notice] = 'Ticket was successfully created.' if @ticket.save + respond_with(@ticket) + + end + +=begin + def edit + @ticket = Ticket.find(params[:id]) + @ticket.comments.build + # build ticket comments? + end +=end + + def show + @ticket = Ticket.find(params[:id]) + # @ticket.comments.build + # build ticket comments? + end + + def update + @ticket = Ticket.find(params[:id]) + @ticket.attributes = params[:ticket] + + @ticket.comments.last.posted_by = (current_user ? current_user.id : nil) #protecting posted_by isn't working, so this should protect it. + + if @ticket.save + flash[:notice] = 'Ticket was successfully updated.' + respond_with @ticket + else + #redirect_to [:show, @ticket] # + flash[:alert] = 'Ticket has not been changed' + redirect_to @ticket + #respond_with(@ticket) # why does this go to edit?? redirect??? + end + end + + def index + # @tickets = Ticket.by_title #not actually what we will want + respond_with(@tickets = Ticket.all) #we'll want only tickets that this user can access + end + + private + + # not using now, as we are using comment_attributes= from the Ticket model +=begin + def add_comment + comment = TicketComment.new(params[:comment]) + comment.posted_by = User.current.id if User.current #could be nil + comment.posted_at = Time.now # TODO: it seems strange to have this here, and not in model + @ticket.comments << comment + end +=end +end diff --git a/help/app/models/ticket.rb b/help/app/models/ticket.rb index 784d7ef..f38fed2 100644 --- a/help/app/models/ticket.rb +++ b/help/app/models/ticket.rb @@ -15,8 +15,8 @@ class Ticket < CouchRest::Model::Base =end #belongs_to :user #from leap_web_users. doesn't necessarily belong to a user though - property :created_by, Integer #nil unless user was authenticated for ticket creation, #THIS should not be changed after being set - property :regarding_user, Integer # form cannot be submitted if they type in a username w/out corresponding ID. this field can be nil. for authenticated ticket creation by non-admins, should this just automatically be set to be same as created_by? or maybe we don't use this field unless created_by is nil? + property :created_by, String, :protected => true #Integer #nil unless user was authenticated for ticket creation, #THIS should not be changed after being set + #property :regarding_user, String#Integer # form cannot be submitted if they type in a username w/out corresponding ID. this field can be nil. for authenticated ticket creation by non-admins, should this just automatically be set to be same as created_by? or maybe we don't use this field unless created_by is nil? #also, both created_by and regarding_user could be nil---say user forgets username, or has general question property :title, String property :email, String #verify @@ -29,18 +29,27 @@ class Ticket < CouchRest::Model::Base timestamps! - before_validation :set_created_by, :set_code, :on => :create + #before_validation :set_created_by, :set_code, :set_email, :on => :create + before_validation :set_code, :set_email, :on => :create + + + #named_scope :open, :conditions => {:is_open => true} #?? design do view :by_title end + validates :title, :presence => true + #validates :comments, :presence => true #do we want it like this? + + # html5 has built-in validation which isn't ideal, as it says 'please enter an email address' for invalid email addresses, which implies an email address is required, and it is not. validates :email, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, :if => :email #email address is optional - def set_created_by - self.created_by = User.current if User.current - end + #TODO: + #def set_created_by + # self.created_by = User.current if User.current + #end def is_creator_validated? !!created_by @@ -51,6 +60,12 @@ class Ticket < CouchRest::Model::Base self.code = SecureRandom.hex(8) if !is_creator_validated? end + + def set_email + self.email = nil if self.email == "" + # in controller set to be current users email if that exists + end + def close self.is_open = false save @@ -61,6 +76,15 @@ class Ticket < CouchRest::Model::Base save end + def comments_attributes=(attributes) + + comment = TicketComment.new(attributes.values.first) #TicketComment.new(attributes) + #comment.posted_by = User.current.id if User.current #we want to avoid User.current, and current_user won't work here. instead will set in tickets_controller + comment.posted_at = Time.now + comments << comment + + end + =begin def validate if email_address and not email_address.strip =~ RFC822::EmailAddress diff --git a/help/app/models/ticket_comment.rb b/help/app/models/ticket_comment.rb index 652133a..49e5c6c 100644 --- a/help/app/models/ticket_comment.rb +++ b/help/app/models/ticket_comment.rb @@ -2,13 +2,15 @@ class TicketComment include CouchRest::Model::Embeddable #belongs_to :ticket #is this best way to do it? will want to access all of a tickets comments, so maybe this isn't the way? - property :posted_by, Integer, :protected => true# maybe this should be current_user if that is set, meaning the user is logged in #String # user?? + property :posted_by, String#, :protected => true #Integer#this should be current_user if that is set, meaning the user is logged in #cannot have it be protected and set via comments_attributes=. also, if it is protected and we set in the tickets_controller, it gets unset. TODO---is this okay to have it not protected and manually check it? We do not users to be able to set this. # if the current user is not set, then we could just say the comment comes from an 'unauthenticated user', which would be somebody with the secret URL - property :posted_at, Time, :protected => true + property :posted_at, Time#, :protected => true #property :posted_verified, TrueClass, :protected => true #should be true if current_user is set when the comment is created property :body, String - before_validation :set_time#, :set_posted_by + # ? timestamps! + validates :body, :presence => true + #before_validation :set_time#, :set_posted_by #design do # view :by_posted_at @@ -18,10 +20,14 @@ class TicketComment def is_comment_validated? !!posted_by end - + +=begin + #TODO. + #this is resetting all comments associated with the ticket: def set_time self.posted_at = Time.now end +=end =begin def set_posted_by diff --git a/help/app/views/tickets/_comment.html.haml b/help/app/views/tickets/_comment.html.haml new file mode 100644 index 0000000..1ba3bd1 --- /dev/null +++ b/help/app/views/tickets/_comment.html.haml @@ -0,0 +1,13 @@ +- # style is super ugly but just for now +%div{:style => "border: solid 1px"} + - if User.find(comment.posted_by) + Posted by + = User.find(comment.posted_by).login + - else + Unauthenticated post + %p + Posted at + = comment.posted_at + %p + = comment.body + %p
\ No newline at end of file diff --git a/help/app/views/tickets/_new_comment.html.haml b/help/app/views/tickets/_new_comment.html.haml new file mode 100644 index 0000000..a924dfd --- /dev/null +++ b/help/app/views/tickets/_new_comment.html.haml @@ -0,0 +1,3 @@ += #do we want this partial? not using it now += simple_fields_for :comment do |c| + = c.input :body, :label => 'Comment', :as => :text diff --git a/help/app/views/tickets/index.html.haml b/help/app/views/tickets/index.html.haml new file mode 100644 index 0000000..6db2140 --- /dev/null +++ b/help/app/views/tickets/index.html.haml @@ -0,0 +1,10 @@ +%h2 tickets index (just as space) +Create a += link_to "new ticket", new_ticket_path += # below shouldn't be unless logged in +%h2 Tickets += # want to have selection option to see tickets, that are open, closed or all +- @tickets.each do |ticket| + %p + = link_to ticket.title, ticket += #render(:partial => "ticket", :collection => @tickets) diff --git a/help/app/views/tickets/new.html.haml b/help/app/views/tickets/new.html.haml new file mode 100644 index 0000000..537b97f --- /dev/null +++ b/help/app/views/tickets/new.html.haml @@ -0,0 +1,16 @@ +%h2=t :new_ticket += simple_form_for(@ticket, :html => {:novalidate => true}) do |f| #turn off html5 validations to test + = #@ticket.errors.messages + = f.input :title + = #f.input :email #if there is no current_user + = f.input :email if !current_user #hmm--might authenticated users want to submit an alternate email? + + = f.simple_fields_for :comments do |c| + = c.input :body, :label => 'Comment', :as => :text + + = #render :partial => 'new_comment' #what we were using + = # regarding_user if not logged in + = # email if not logged in + = #f.button :submit, :value => t(:submit), :class => 'btn-primary' + = f.button :submit + = link_to t(:cancel), tickets_path, :class => :btn diff --git a/help/app/views/tickets/show.html.haml b/help/app/views/tickets/show.html.haml new file mode 100644 index 0000000..a9b994e --- /dev/null +++ b/help/app/views/tickets/show.html.haml @@ -0,0 +1,26 @@ +- if flash[:notice] + =flash[:notice] +- if flash[:alert] + =flash[:alert] +%h2= @ticket.title +is open? += @ticket.is_open +- if @ticket.code + code: + = @ticket.code +- if @ticket.email + email: + = @ticket.email +- if User.find(@ticket.created_by) + Created by + = User.find(@ticket.created_by).login +- else + Unauthenticated ticket creator += render(:partial => "comment", :collection => @ticket.comments) + += simple_form_for (@ticket, :html => {:novalidate => true}) do |f| #turn off html5 validations to test + = f.simple_fields_for :comments, TicketComment.new do |c| + = c.input :body, :label => 'Comment', :as => :text + = #render :partial => 'new_comment' + = f.button :submit + = link_to t(:cancel), tickets_path, :class => :btn
\ No newline at end of file diff --git a/help/config/routes.rb b/help/config/routes.rb index 1daf9a4..5e57e02 100644 --- a/help/config/routes.rb +++ b/help/config/routes.rb @@ -1,2 +1,5 @@ Rails.application.routes.draw do + + resources :tickets, :only => [:new, :create, :index, :show, :update] + #resources :ticket, :only => [:show] end diff --git a/help/test/functional/tickets_controller_test.rb b/help/test/functional/tickets_controller_test.rb new file mode 100644 index 0000000..b9e03ac --- /dev/null +++ b/help/test/functional/tickets_controller_test.rb @@ -0,0 +1,64 @@ +require 'test_helper' + +class TicketsControllerTest < ActionController::TestCase + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:tickets) + end + + test "should get new" do + get :new + assert_equal Ticket, assigns(:ticket).class + assert_response :success + end + + + test "should create unauthenticated ticket" do + params = {:title => "ticket test title", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} + + assert_difference('Ticket.count') do + post :create, :ticket => params + end + + assert_response :redirect + #assert_equal assigns(:ticket).email, User.current.email + #assert_equal User.find(assigns(:ticket).created_by).login, User.current.login + assert_nil assigns(:ticket).created_by + + assert_equal 1, assigns(:ticket).comments.count + end + + + test "should create authenticated ticket" do + + params = {:title => "ticket test title", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}} + + login :email => "test@email.net" + + assert_difference('Ticket.count') do + post :create, :ticket => params + end + + assert_response :redirect + ticket = assigns(:ticket) + assert ticket + assert_equal @current_user.id, ticket.created_by + assert_equal @current_user.email, ticket.email + + assert_equal 1, assigns(:ticket).comments.count + end + + test "add comment to ticket" do + + ticket = Ticket.last + assert_difference('Ticket.last.comments.count') do + put :update, :id => ticket.id, + :ticket => {:comments_attributes => {"0" => {"body" =>"NEWER comment"}} } + end + assert_equal ticket, assigns(:ticket) + + end + +end diff --git a/help/test/unit/ticket_comment_test.rb b/help/test/unit/ticket_comment_test.rb index 883720f..1fe1fe2 100644 --- a/help/test/unit/ticket_comment_test.rb +++ b/help/test/unit/ticket_comment_test.rb @@ -16,8 +16,8 @@ class TicketCommentTest < ActiveSupport::TestCase comment2 = TicketComment.new :body => "help my email is broken!" assert comment2.valid? - assert_not_nil comment2.posted_at - assert_nil comment2.posted_by #if not logged in + #assert_not_nil comment2.posted_at #? + #assert_nil comment2.posted_by #if not logged in #TODO #comment.ticket = testticket #Ticket.find_by_title("testing") #assert_equal testticket.title, comment.ticket.title @@ -49,9 +49,10 @@ class TicketCommentTest < ActiveSupport::TestCase testticket.comments << comment2 #this should validate comment2 testticket.valid? assert_equal testticket.comments.count, 2 - assert_not_nil comment.posted_at - assert_not_nil testticket.comments.last.posted_at - assert testticket.comments.first.posted_at < testticket.comments.last.posted_at + # where should posted_at be set? + #assert_not_nil comment.posted_at + #assert_not_nil testticket.comments.last.posted_at + #assert testticket.comments.first.posted_at < testticket.comments.last.posted_at end end diff --git a/help/test/unit/ticket_test.rb b/help/test/unit/ticket_test.rb index c3a4759..6b63a23 100644 --- a/help/test/unit/ticket_test.rb +++ b/help/test/unit/ticket_test.rb @@ -41,18 +41,20 @@ class TicketTest < ActiveSupport::TestCase assert @sample.is_creator_validated? end +=begin +# TODO: do once have current_user stuff in order test "code if & only if not creator-validated" do + User.current_test = nil t1 = Ticket.create :title => 'test title' assert_not_nil t1.code assert_nil t1.created_by - User.current = 4 + User.current_test = 4 t2 = Ticket.create :title => 'test title' assert_nil t2.code assert_not_nil t2.created_by - - end +=end end diff --git a/public/config/eip-service.json b/public/config/eip-service.json deleted file mode 100644 index 29959d7..0000000 --- a/public/config/eip-service.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "serial": 1, - "version": "0.1.0", - "capabilities": { - "transport": ["openvpn"], - "ports": ["80","53", "443", "1194"], - "protocols": ["udp"], - "static_ips": false, - "adblock": false - }, - "gateways": [ - {"country_code": "tr", "name": "turkey", "label": {"en":"Ankara, Turkey"}, "capabilities": {}, "hosts": ["94.103.43.4"]} - ] -} diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb index e8065d9..be7aa1f 100644 --- a/test/dummy/app/controllers/application_controller.rb +++ b/test/dummy/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ class ApplicationController < ActionController::Base protect_from_forgery + + ActiveSupport.run_load_hooks(:application_controller, self) end diff --git a/test/test_helper.rb b/test/test_helper.rb index f7d48ec..0016771 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,7 +2,10 @@ ENV["RAILS_ENV"] = "test" require File.expand_path('../../config/environment', __FILE__) require 'rails/test_help' -require 'mocha' +require 'mocha/setup' + +# Load support files from all engines +Dir["#{File.dirname(__FILE__)}/../*/test/support/**/*.rb"].each { |f| require f } class ActiveSupport::TestCase # Add more helper methods to be used by all tests here... diff --git a/ui_dependencies.rb b/ui_dependencies.rb index 21de1c6..454e9a8 100644 --- a/ui_dependencies.rb +++ b/ui_dependencies.rb @@ -1,7 +1,9 @@ gem "haml", "~> 3.1.7" -gem "bootstrap-sass", "~> 2.0.4" +gem "bootstrap-sass", "~> 2.1.0" gem "jquery-rails" gem "simple_form" +gem 'client_side_validations' +gem 'client_side_validations-simple_form' group :assets do gem "haml-rails", "~> 0.3.4" @@ -10,7 +12,7 @@ group :assets do gem "uglifier", "~> 1.2.7" # See https://github.com/sstephenson/execjs#readme for more supported runtimes - gem 'therubyracer', :platforms => :ruby + gem 'therubyracer', "~> 0.10.2", :platforms => :ruby end diff --git a/users/app/assets/javascripts/srp b/users/app/assets/javascripts/srp -Subproject d6a78049f3356d9d645143362eca74434410bf6 +Subproject fff770a866b44abce6fe0fc5d5ffde034225436 diff --git a/users/app/assets/javascripts/users.js.coffee b/users/app/assets/javascripts/users.js.coffee index 160a7f0..76a6d79 100644 --- a/users/app/assets/javascripts/users.js.coffee +++ b/users/app/assets/javascripts/users.js.coffee @@ -1,41 +1,35 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ -# +preventDefault = (event) -> + event.preventDefault() -validate_password = (event) -> +srp.session = new srp.Session() +srp.signedUp = -> + srp.login - password = $('#srp_password').val() - confirmation = $('#srp_password_confirmation').val() - login = $('#srp_username').val() +srp.loggedIn = -> + window.location = '/' - if password != confirmation - alert "Password and Confirmation do not match!" - $('#srp_password').focus() - return false - if password == login - alert "Password and Login may not match!" - $('#srp_password').focus() - return false - if password.length < 8 - alert "Password needs to be at least 8 characters long!" - $('#srp_password').focus() - return false - - return true - +#// TODO: not sure this is what we want. +srp.updated = -> + window.location = '/' -insert_verifier = (event) -> - # TODO: verify password confimation - srp = new SRP - salt = srp.session.getSalt() - $('#srp_salt').val(salt) - $('#srp_password_verifier').val(srp.session.getV().toString(16)) - # clear the password so we do not submit it - $('#srp_password').val('cleared out - use verifier instead') - $('#srp_password_confirmation').val('using srp - store verifier') +srp.error = (message) -> + if $.isPlainObject(message) && message.errors + for field, error of message.errors + element = $('form input[name$="['+field+']"]') + next unless element + element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false) + else + alert(message) + +pollUsers = (query, process) -> + $.get( "/users.json", query: query).done(process) $(document).ready -> - $('#new_user').submit validate_password - $('#new_user').submit insert_verifier + $('#new_user').submit preventDefault + $('#new_user').submit srp.signup + $('#new_session').submit preventDefault + $('#new_session').submit srp.login + $('.user.form.edit').submit srp.update + $('.user.form.edit').submit preventDefault + $('.user.typeahead').typeahead({source: pollUsers}); diff --git a/users/app/controllers/controller_extension/authentication.rb b/users/app/controllers/controller_extension/authentication.rb new file mode 100644 index 0000000..6ac7a5b --- /dev/null +++ b/users/app/controllers/controller_extension/authentication.rb @@ -0,0 +1,38 @@ +module ControllerExtension::Authentication + extend ActiveSupport::Concern + + private + + included do + helper_method :current_user, :logged_in?, :admin? + end + + def authentication_errors + return unless errors = warden.winning_strategy.try(:message) + errors.inject({}) do |translated,err| + translated[err.first] = I18n.t(err.last) + translated + end + end + + def logged_in? + !!current_user + end + + def authorize + access_denied unless logged_in? + end + + def access_denied + redirect_to login_url, :alert => "Not authorized" + end + + def admin? + current_user && current_user.is_admin? + end + + def authorize_admin + access_denied unless admin? + end + +end diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb index 284c0e2..bc910b5 100644 --- a/users/app/controllers/sessions_controller.rb +++ b/users/app/controllers/sessions_controller.rb @@ -3,33 +3,24 @@ class SessionsController < ApplicationController skip_before_filter :verify_authenticity_token def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end end def create - @user = User.find_by_param(params[:login]) - session[:handshake] = @user.initialize_auth(params['A'].hex) - User.current = @user #? - render :json => session[:handshake] - rescue RECORD_NOT_FOUND - render :json => {:errors => {:login => ["unknown user"]}} + authenticate! end def update - # TODO: validate the id belongs to the session - @user = User.find_by_param(params[:id]) - @srp_session = session.delete(:handshake) - @srp_session.authenticate!(params[:client_auth].hex) - session[:user_id] = @user.id - User.current = @user #? - render :json => @srp_session - rescue WRONG_PASSWORD - session[:handshake] = nil - render :json => {:errors => {"password" => ["wrong password"]}} + authenticate! + render :json => session.delete(:handshake) end def destroy - session[:user_id] = nil - User.current = nil #? + logout redirect_to root_path end end diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb index 82d2eac..cffc8c6 100644 --- a/users/app/controllers/users_controller.rb +++ b/users/app/controllers/users_controller.rb @@ -1,18 +1,48 @@ class UsersController < ApplicationController - skip_before_filter :verify_authenticity_token + skip_before_filter :verify_authenticity_token, :only => [:create] + + before_filter :fetch_user, :only => [:edit, :update, :destroy] + before_filter :authorize_admin, :only => [:index] respond_to :json, :html + def index + if params[:query] + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + else + @users = User.by_created_at.descending + end + @users = @users.limit(10) + respond_with @users.map(&:login).sort + end + def new @user = User.new end def create - @user = User.create!(params[:user]) - respond_with(@user, :location => root_url, :notice => "Signed up!") - rescue VALIDATION_FAILED => e - @user = e.document - respond_with(@user, :location => new_user_path) + @user = User.create(params[:user]) + respond_with @user + end + + def edit + end + + def update + @user.update_attributes(params[:user]) + respond_with @user + end + + def destroy + @user.destroy + redirect_to admin? ? users_path : login_path + end + + protected + + def fetch_user + @user = User.find_by_param(params[:id]) + access_denied unless admin? or (@user == current_user) end end diff --git a/users/app/models/session.rb b/users/app/models/session.rb new file mode 100644 index 0000000..a9fdb1b --- /dev/null +++ b/users/app/models/session.rb @@ -0,0 +1,34 @@ +class Session < SRP::Session + include ActiveModel::Validations + + attr_accessor :login + + validates :login, + :presence => true, + :format => { :with => /\A[A-Za-z\d_]+\z/, + :message => "Only letters, digits and _ allowed" } + + def initialize(user = nil, aa = nil) + super(user, aa) if user + end + + def persisted? + false + end + + def new_record? + true + end + + def to_model + self + end + + def to_key + [object_id] + end + + def to_param + nil + end +end diff --git a/users/app/models/user.rb b/users/app/models/user.rb index 1afb9db..325c981 100644 --- a/users/app/models/user.rb +++ b/users/app/models/user.rb @@ -9,42 +9,46 @@ class User < CouchRest::Model::Base :presence => true validates :login, - :uniqueness => true + :uniqueness => true, + :if => :serverside? validates :login, :format => { :with => /\A[A-Za-z\d_]+\z/, :message => "Only letters, digits and _ allowed" } validates :password_salt, :password_verifier, - :format => { :with => /\A[\dA-Fa-f]+\z/, - :message => "Only hex numbers allowed" } + :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } + + validates :password, :presence => true, + :confirmation => true, + :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } timestamps! design do view :by_login + view :by_created_at end class << self - def find_by_param(login) - return find_by_login(login) || raise(RECORD_NOT_FOUND) - end + alias_method :find_by_param, :find # valid set of attributes for testing def valid_attributes_hash { :login => "me", - :password_verifier => "1234ABC", + :password_verifier => "1234ABCD", :password_salt => "4321AB" } end end - def to_param - self.login - end + alias_method :to_param, :id def to_json(options={}) - super(options.merge(:only => ['login', 'password_salt'])) + { + :login => login, + :ok => valid? + }.to_json(options) end def initialize_auth(aa) @@ -63,11 +67,18 @@ class User < CouchRest::Model::Base login end - def self.current - Thread.current[:user] + # Since we are storing admins by login, we cannot allow admins to change their login. + def is_admin? + APP_CONFIG['admins'].include? self.login end - def self.current=(user) - Thread.current[:user] = user + + protected + def password + password_verifier end + # used as a condition for validations that are server side only + def serverside? + true + end end diff --git a/users/app/views/sessions/_admin_nav.html.haml b/users/app/views/sessions/_admin_nav.html.haml new file mode 100644 index 0000000..14dfbdc --- /dev/null +++ b/users/app/views/sessions/_admin_nav.html.haml @@ -0,0 +1,6 @@ +%a#admin-menu{"data-toggle" => "dropdown", :role => :button} + Admin +%ul.dropdown-menu{:role => "menu", "aria-labelledby" => "admin-menu"} + %li + = link_to Ticket.model_name.human(:count => ""), tickets_path, {:tabindex => -1} + = link_to User.model_name.human(:count => ""), users_path, {:tabindex => -1} diff --git a/users/app/views/sessions/_nav.html.haml b/users/app/views/sessions/_nav.html.haml new file mode 100644 index 0000000..5306d0e --- /dev/null +++ b/users/app/views/sessions/_nav.html.haml @@ -0,0 +1,13 @@ +- if logged_in? + - if admin? + %li.dropdown + = render 'sessions/admin_nav' + %li + = link_to current_user.login, edit_user_path(current_user) + %li + = link_to t(:logout), logout_path +- else + %li + = link_to t(:login), login_path + %li + = link_to t(:signup), signup_path diff --git a/users/app/views/sessions/new.html.haml b/users/app/views/sessions/new.html.haml index 39ee7bf..a04f584 100644 --- a/users/app/views/sessions/new.html.haml +++ b/users/app/views/sessions/new.html.haml @@ -1,7 +1,8 @@ -%h2=t :login -= simple_form_for :session, :url => sessions_path, :html => { :id => :new_session } do |f| - %legend=t :login_message - = f.input :login, :input_html => { :id => :srp_username } - = f.input :password, :required => true, :input_html => { :id => :srp_password } - = f.button :submit, :value => t(:login), :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn +.span8.offset2 + %h2=t :login + = simple_form_for @session, :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| + %legend=t :login_message + = f.input :login, :input_html => { :id => :srp_username } + = f.input :password, :required => true, :input_html => { :id => :srp_password } + = f.button :submit, :value => t(:login), :class => 'btn-primary' + = link_to t(:cancel), root_url, :class => :btn diff --git a/users/app/views/sessions/new.json.erb b/users/app/views/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/users/app/views/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/users/app/views/users/_cancel_account.html.haml b/users/app/views/users/_cancel_account.html.haml new file mode 100644 index 0000000..41580b0 --- /dev/null +++ b/users/app/views/users/_cancel_account.html.haml @@ -0,0 +1,6 @@ +%legend + =t :cancel_account + %small You will not be able to login anymore. += link_to user_path(@user), :method => :delete, :class => "btn btn-danger" do + %i.icon-remove.icon-white + Remove my Account diff --git a/users/app/views/users/_form.html.haml b/users/app/views/users/_form.html.haml new file mode 100644 index 0000000..39e26a6 --- /dev/null +++ b/users/app/views/users/_form.html.haml @@ -0,0 +1,15 @@ +- only = local_assigns[:only] +- html = {:class => 'form-horizontal user form ' + (@user.new_record? ? 'new' : 'edit')} += simple_form_for @user, :validate => true, :format => :json, :html => html do |f| + %legend + = t(only || :signup_message) + - if !only || only == :change_login + = f.input :login, :input_html => { :id => :srp_username } + - if !only || only == :change_password + = f.input :password, :required => true, :validate => true, :input_html => { :id => :srp_password } + = f.input :password_confirmation, :required => true, :input_html => { :id => :srp_password_confirmation } + .pull-right + = f.button :submit, :class => 'btn-primary' + - unless only + = link_to t(:cancel), root_url, :class => :btn + .clearfix diff --git a/users/app/views/users/_user.html.haml b/users/app/views/users/_user.html.haml new file mode 100644 index 0000000..7db0041 --- /dev/null +++ b/users/app/views/users/_user.html.haml @@ -0,0 +1,10 @@ +%tr + %td= user.login + %td= time_ago_in_words(user.created_at) + " ago" + %td + = link_to edit_user_path(user), :class => "btn btn-mini btn-primary" do + %i.icon-edit.icon-white + Edit + = link_to user_path(user), :method => :delete, :class => "btn btn-danger btn-mini" do + %i.icon-remove.icon-white + Remove diff --git a/users/app/views/users/edit.html.haml b/users/app/views/users/edit.html.haml new file mode 100644 index 0000000..25da71a --- /dev/null +++ b/users/app/views/users/edit.html.haml @@ -0,0 +1,5 @@ +.span8.offset2 + %h2=t :settings + = render :partial => 'form', :locals => {:only => :change_login} + = render :partial => 'form', :locals => {:only => :change_password} + = render 'cancel_account' if @user == current_user diff --git a/users/app/views/users/index.html.haml b/users/app/views/users/index.html.haml new file mode 100644 index 0000000..9e6a179 --- /dev/null +++ b/users/app/views/users/index.html.haml @@ -0,0 +1,17 @@ +.page-header + %h1= User.model_name.human(:count =>User.count) +.row + .span8 + %h2= params[:query] ? "Users starting with '#{params[:query]}'" : "Last users who signed up" + %table.table.table-hover + %tr + %th Login + %th Created + %th Action + = render @users.all + .span4 + %h4 Find user + = form_tag users_path, :method => :get, :class => "form-search" do + .input-append + = text_field_tag :query, "", :class => "user typeahead span2 search-query", :autocomplete => :off + %button.btn{:type => :submit} Search diff --git a/users/app/views/users/new.html.haml b/users/app/views/users/new.html.haml index f6ece3a..c1c4208 100644 --- a/users/app/views/users/new.html.haml +++ b/users/app/views/users/new.html.haml @@ -1,10 +1,3 @@ -%h2=t :signup -= simple_form_for @user do |f| - %legend=t :signup_message - = f.input :login, :input_html => { :id => :srp_username } - = f.input :password, :required => true, :input_html => { :id => :srp_password } - = f.input :password_confirmation, :required => true, :input_html => { :id => :srp_password_confirmation } - = f.input :password_verifier, :as => :hidden, :input_html => { :id => :srp_password_verifier } - = f.input :password_salt, :as => :hidden, :input_html => { :id => :srp_salt } - = f.button :submit, :value => t(:signup), :class => 'btn-primary' - = link_to t(:cancel), root_url, :class => :btn +.span8.offset2 + %h2=t :signup + = render 'form' diff --git a/users/config/initializers/add_controller_methods.rb b/users/config/initializers/add_controller_methods.rb new file mode 100644 index 0000000..2579176 --- /dev/null +++ b/users/config/initializers/add_controller_methods.rb @@ -0,0 +1,3 @@ +ActiveSupport.on_load(:application_controller) do + include ControllerExtension::Authentication +end diff --git a/users/config/initializers/warden.rb b/users/config/initializers/warden.rb new file mode 100644 index 0000000..45feb6c --- /dev/null +++ b/users/config/initializers/warden.rb @@ -0,0 +1,7 @@ +Rails.configuration.middleware.use RailsWarden::Manager do |config| + config.default_strategies :secure_remote_password + config.failure_app = SessionsController +end + +RailsWarden.unauthenticated_action = :new + diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml new file mode 100644 index 0000000..1260494 --- /dev/null +++ b/users/config/locales/en.yml @@ -0,0 +1,14 @@ +en: + signup: "Sign up" + signup_message: "Please create an account." + cancel: "Cancel" + login: "Login" + login_message: "Please login with your account." + wrong_password: "wrong password" + user_not_found: "could not be found" + + activemodel: + models: + user: + one: User + other: "%{count} Users" diff --git a/users/config/routes.rb b/users/config/routes.rb index cfc0407..1d144b4 100644 --- a/users/config/routes.rb +++ b/users/config/routes.rb @@ -1,10 +1,10 @@ Rails.application.routes.draw do - get "log_in" => "sessions#new", :as => "log_in" - get "log_out" => "sessions#destroy", :as => "log_out" + get "login" => "sessions#new", :as => "login" + get "logout" => "sessions#destroy", :as => "logout" resources :sessions, :only => [:new, :create, :update, :destroy] - get "sign_up" => "users#new", :as => "sign_up" - resources :users, :only => [:new, :create] + get "signup" => "users#new", :as => "signup" + resources :users end diff --git a/users/leap_web_users.gemspec b/users/leap_web_users.gemspec index f64a76a..0682a99 100644 --- a/users/leap_web_users.gemspec +++ b/users/leap_web_users.gemspec @@ -17,5 +17,6 @@ Gem::Specification.new do |s| s.add_dependency "leap_web_core", LeapWeb::VERSION - s.add_dependency "ruby-srp", "~> 0.1.1" + s.add_dependency "ruby-srp", "~> 0.1.4" + s.add_dependency "rails_warden" end diff --git a/users/lib/leap_web_users/engine.rb b/users/lib/leap_web_users/engine.rb index 9b7545e..7033576 100644 --- a/users/lib/leap_web_users/engine.rb +++ b/users/lib/leap_web_users/engine.rb @@ -1,8 +1,12 @@ # thou shall require all your dependencies in an engine. require "leap_web_core" require "leap_web_core/ui_dependencies" +require "rails_warden" require "ruby-srp" +require "warden/session_serializer" +require "warden/strategies/secure_remote_password" + module LeapWebUsers class Engine < ::Rails::Engine diff --git a/users/lib/warden/session_serializer.rb b/users/lib/warden/session_serializer.rb new file mode 100644 index 0000000..81d7076 --- /dev/null +++ b/users/lib/warden/session_serializer.rb @@ -0,0 +1,13 @@ +module Warden + # Setup Session Serialization + class SessionSerializer + def serialize(record) + [record.class.name, record.id] + end + + def deserialize(keys) + klass, id = keys + klass.constantize.find(id) + end + end +end diff --git a/users/lib/warden/strategies/secure_remote_password.rb b/users/lib/warden/strategies/secure_remote_password.rb new file mode 100644 index 0000000..594e27e --- /dev/null +++ b/users/lib/warden/strategies/secure_remote_password.rb @@ -0,0 +1,58 @@ +module Warden + module Strategies + class SecureRemotePassword < Warden::Strategies::Base + + def valid? + handshake? || authentication? + end + + def authenticate! + if authentication? + validate! + else # handshake + initialize! + end + end + + protected + + def handshake? + params['A'] && params['login'] + end + + def authentication? + params['client_auth'] && session[:handshake] + end + + def validate! + user = session[:handshake].authenticate(params['client_auth'].hex) + user ? success!(user) : fail!(:password => "wrong_password") + end + + def initialize! + if user = User.find_by_login(id) + session[:handshake] = user.initialize_auth(params['A'].hex) + custom! json_response(session[:handshake]) + else + fail! :login => "user_not_found" + end + end + + def json_response(object) + [ 200, + {"Content-Type" => "application/json; charset=utf-8"}, + [object.to_json] + ] + end + + def id + params["id"] || params["login"] + end + end + end + Warden::Strategies.add :secure_remote_password, + Warden::Strategies::SecureRemotePassword + +end + + diff --git a/users/test/functional/application_controller_test.rb b/users/test/functional/application_controller_test.rb new file mode 100644 index 0000000..857bae5 --- /dev/null +++ b/users/test/functional/application_controller_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class ApplicationControllerTest < ActionController::TestCase + + def setup + # so we can test the effect on the response + @controller.response = @response + end + + def test_authorize_redirect + @controller.send(:authorize) + assert_access_denied + end + + def test_authorized + login + @controller.send(:authorize) + assert_access_denied(false) + end + + def test_authorize_admin + login + @current_user.expects(:is_admin?).returns(false) + @controller.send(:authorize_admin) + assert_access_denied + end + +end diff --git a/users/test/functional/helper_methods_test.rb b/users/test/functional/helper_methods_test.rb new file mode 100644 index 0000000..2b2375c --- /dev/null +++ b/users/test/functional/helper_methods_test.rb @@ -0,0 +1,39 @@ +# +# Testing and documenting the helper methods available from +# ApplicationController +# + +require 'test_helper' + +class HelperMethodsTest < ActionController::TestCase + tests ApplicationController + + # we test them right in here... + include ApplicationController._helpers + + # they all reference the controller. + def controller + @controller + end + + def test_current_user + login + assert_equal @current_user, current_user + end + + def test_logged_in + login + assert logged_in? + end + + def test_logged_out + assert !logged_in? + end + + def test_admin + login + @current_user.expects(:is_admin?).returns(bool = stub) + assert_equal bool, admin? + end + +end diff --git a/users/test/functional/sessions_controller_test.rb b/users/test/functional/sessions_controller_test.rb index b6e56a7..9df4455 100644 --- a/users/test/functional/sessions_controller_test.rb +++ b/users/test/functional/sessions_controller_test.rb @@ -1,79 +1,72 @@ require 'test_helper' +# This is a simple controller unit test. +# We're stubbing out both warden and srp. +# There's an integration test testing the full rack stack and srp class SessionsControllerTest < ActionController::TestCase - def setup + setup do + @user = stub :login => "me", :id => 123 @client_hex = 'a123' - @client_rnd = @client_hex.hex - @server_hex = 'b123' - @server_rnd = @server_hex.hex - @server_rnd_exp = 'e123'.hex - @salt = 'stub user salt' - @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp - @server_auth = 'adfe' end test "should get login screen" do + request.env['warden'].expects(:winning_strategy) get :new assert_response :success + assert_equal "text/html", response.content_type + assert_template "sessions/new" end - test "should perform handshake" do - user = stub :login => "me", :id => 123 - user.expects(:initialize_auth). - with(@client_rnd). - returns(@server_handshake) - @server_handshake.expects(:to_json). - returns({'B' => @server_hex, 'salt' => @salt}.to_json) - User.expects(:find_by_param).with(user.login).returns(user) - post :create, :login => user.login, 'A' => @client_hex - assert_equal @server_handshake, session[:handshake] + test "renders json" do + request.env['warden'].expects(:winning_strategy) + get :new, :format => :json assert_response :success - assert_json_response :B => @server_hex, :salt => @salt + assert_json_error nil end - test "should report user not found" do - unknown = "login_that_does_not_exist" - User.expects(:find_by_param).with(unknown).raises(RECORD_NOT_FOUND) - post :create, :login => unknown - assert_response :success - assert_json_response :errors => {"login" => ["unknown user"]} + test "renders warden errors" do + strategy = stub :message => {:field => :translate_me} + request.env['warden'].stubs(:winning_strategy).returns(strategy) + I18n.expects(:t).with(:translate_me).at_least_once.returns("translation stub") + get :new, :format => :json + assert_response 422 + assert_json_error :field => "translation stub" end - test "should authorize" do - session[:handshake] = @server_handshake - user = stub :login => "me", :id => 123 - @server_handshake.expects(:authenticate!). - with(@client_rnd). - returns(@server_auth) - @server_handshake.expects(:to_json). - returns({:M2 => @server_auth}.to_json) - User.expects(:find_by_param).with(user.login).returns(user) - post :update, :id => user.login, :client_auth => @client_hex - assert_nil session[:handshake] - assert_json_response :M2 => @server_auth - assert_equal user.id, session[:user_id] + # Warden takes care of parsing the params and + # rendering the response. So not much to test here. + test "should perform handshake" do + request.env['warden'].expects(:authenticate!) + # make sure we don't get a template missing error: + @controller.stubs(:render) + post :create, :login => @user.login, 'A' => @client_hex end - test "should report wrong password" do - session[:handshake] = @server_handshake - user = stub :login => "me", :id => 123 - @server_handshake.expects(:authenticate!). - with(@client_rnd). - raises(WRONG_PASSWORD) - User.expects(:find_by_param).with(user.login).returns(user) - post :update, :id => user.login, :client_auth => @client_hex + test "should authorize" do + request.env['warden'].expects(:authenticate!) + handshake = stub(:to_json => "JSON") + session[:handshake] = handshake + post :update, :id => @user.login, :client_auth => @client_hex assert_nil session[:handshake] - assert_nil session[:user_id] - assert_json_response :errors => {"password" => ["wrong password"]} + assert_response :success + assert_equal handshake.to_json, @response.body end - test "logout should reset sessions user_id" do - session[:user_id] = "set" + test "logout should reset warden user" do + expect_warden_logout delete :destroy - assert_nil session[:user_id] assert_response :redirect assert_redirected_to root_url end + def expect_warden_logout + raw = mock('raw session') do + expects(:inspect) + end + request.env['warden'].expects(:raw_session).returns(raw) + request.env['warden'].expects(:logout) + end + + end diff --git a/users/test/functional/users_controller_test.rb b/users/test/functional/users_controller_test.rb index 1cb28a6..939d105 100644 --- a/users/test/functional/users_controller_test.rb +++ b/users/test/functional/users_controller_test.rb @@ -1,33 +1,132 @@ require 'test_helper' class UsersControllerTest < ActionController::TestCase + include StubRecordHelper + test "should get new" do get :new + assert_equal User, assigns(:user).class assert_response :success end test "should create new user" do - params = User.valid_attributes_hash - user = stub params.merge(:id => 123) - params.stringify_keys! - User.expects(:create!).with(params).returns(user) - post :create, :user => params + user = stub_record User + User.expects(:create).with(user.params).returns(user) + + post :create, :user => user.params, :format => :json + assert_nil session[:user_id] - assert_response :redirect - assert_redirected_to root_url + assert_json_response user + assert_response :success end test "should redirect to signup form on failed attempt" do params = User.valid_attributes_hash.slice(:login) user = User.new(params) params.stringify_keys! - User.expects(:create!).with(params).raises(VALIDATION_FAILED.new(user)) - post :create, :user => params - assert_nil session[:user_id] + assert !user.valid? + User.expects(:create).with(params).returns(user) + + post :create, :user => params, :format => :json + + assert_json_error user.errors.messages + assert_response 422 + end + + test "should get edit view" do + user = find_record User + + login user + get :edit, :id => user.id + assert_equal user, assigns[:user] + end + + test "should process updated params" do + user = find_record User + user.expects(:update_attributes).with(user.params).returns(true) + + login user + put :update, :user => user.params, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can update user" do + user = find_record User + user.expects(:update_attributes).with(user.params).returns(true) + + login :is_admin? => true + put :update, :user => user.params, :id => user.id, :format => :json + + assert_equal user, assigns[:user] + assert_response 204 + assert_equal " ", @response.body + end + + test "admin can destroy user" do + user = find_record User + user.expects(:destroy) + + login :is_admin? => true + delete :destroy, :id => user.id + + assert_response :redirect + assert_redirected_to users_path + end + + test "user can cancel account" do + user = find_record User + user.expects(:destroy) + + login user + delete :destroy, :id => @current_user.id + assert_response :redirect - assert_redirected_to new_user_path + assert_redirected_to login_path + end + + test "non-admin can't destroy user" do + user = stub_record User + + login + delete :destroy, :id => user.id + + assert_access_denied + end + + test "admin can list users" do + login :is_admin? => true + get :index + + assert_response :success + assert assigns(:users) + end + + test "non-admin can't list users" do + login + get :index + + assert_access_denied + end + + test "admin can autocomplete users" do + login :is_admin? => true + get :index, :format => :json + + assert_response :success + assert assigns(:users) + end + + test "admin can search users" do + login :is_admin? => true + get :index, :query => "a" + + assert_response :success + assert assigns(:users) end end diff --git a/users/test/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb index 66de1e5..add12fe 100644 --- a/users/test/integration/api/account_flow_test.rb +++ b/users/test/integration/api/account_flow_test.rb @@ -1,23 +1,19 @@ require 'test_helper' -class AccountFlowTest < ActionDispatch::IntegrationTest +CONFIG_RU = (Rails.root + 'config.ru').to_s +OUTER_APP = Rack::Builder.parse_file(CONFIG_RU).first - # this test wraps the api and implements the interface the ruby-srp client. - def handshake(login, aa) - post "sessions", :login => login, 'A' => aa.to_s(16) - assert_response :success - response = JSON.parse(@response.body) - if response['errors'] - raise RECORD_NOT_FOUND.new(response['errors']) - else - return response['B'].hex - end +class AccountFlowTest < ActiveSupport::TestCase + include Rack::Test::Methods + include Warden::Test::Helpers + include LeapWebCore::AssertResponses + + def app + OUTER_APP end - def validate(m) - put "sessions/" + @login, :client_auth => m.to_s(16) - assert_response :success - return JSON.parse(@response.body) + def teardown + Warden.test_reset! end def setup @@ -38,13 +34,30 @@ class AccountFlowTest < ActionDispatch::IntegrationTest @user.destroy if @user # make sure we can run this test again end + # this test wraps the api and implements the interface the ruby-srp client. + def handshake(login, aa) + post "/sessions.json", :login => login, 'A' => aa.to_s(16), :format => :json + response = JSON.parse(last_response.body) + if response['errors'] + raise RECORD_NOT_FOUND.new(response['errors']) + else + return response['B'].hex + end + end + + def validate(m) + put "/sessions/" + @login + '.json', :client_auth => m.to_s(16), :format => :json + return JSON.parse(last_response.body) + end + test "signup response" do - assert_json_response @user_params.slice(:login, :password_salt) - assert_response :success + assert_json_response :login => @login, :ok => true + assert last_response.successful? end test "signup and login with srp via api" do server_auth = @srp.authenticate(self) + assert last_response.successful? assert_nil server_auth["errors"] assert server_auth["M2"] end @@ -52,7 +65,8 @@ class AccountFlowTest < ActionDispatch::IntegrationTest test "signup and wrong password login attempt" do srp = SRP::Client.new(@login, "wrong password") server_auth = srp.authenticate(self) - assert_equal ["wrong password"], server_auth["errors"]['password'] + assert_json_error :password => "wrong password" + assert !last_response.successful? assert_nil server_auth["M2"] end @@ -62,6 +76,8 @@ class AccountFlowTest < ActionDispatch::IntegrationTest assert_raises RECORD_NOT_FOUND do server_auth = srp.authenticate(self) end + assert_json_error :login => "could not be found" + assert !last_response.successful? assert_nil server_auth end diff --git a/users/test/integration/api/python/flow_with_srp.py b/users/test/integration/api/python/flow_with_srp.py index 0a11aec..b599252 100755 --- a/users/test/integration/api/python/flow_with_srp.py +++ b/users/test/integration/api/python/flow_with_srp.py @@ -16,7 +16,7 @@ def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for x in range(size)) # using globals for a start -server = 'http://springbok/1/' +server = 'http://springbok.leap.se/1/' login = id_generator() password = id_generator() + id_generator() diff --git a/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb new file mode 100644 index 0000000..f3506ae --- /dev/null +++ b/users/test/support/auth_test_helper.rb @@ -0,0 +1,35 @@ +module AuthTestHelper + include StubRecordHelper + extend ActiveSupport::Concern + + # Controller will fetch current user from warden. + # Make it pick up our current_user + included do + setup do + request.env['warden'] ||= stub :user => nil + end + end + + def login(user_or_method_hash = {}) + @current_user = stub_record(User, user_or_method_hash) + unless @current_user.respond_to? :is_admin? + @current_user.stubs(:is_admin?).returns(false) + end + request.env['warden'] = stub :user => @current_user + return @current_user + end + + def assert_access_denied(denied = true) + if denied + assert_equal({:alert => "Not authorized"}, flash.to_hash) + assert_redirected_to login_path + else + assert flash[:alert].blank? + end + end + +end + +class ActionController::TestCase + include AuthTestHelper +end diff --git a/users/test/support/stub_record_helper.rb b/users/test/support/stub_record_helper.rb new file mode 100644 index 0000000..2e1a533 --- /dev/null +++ b/users/test/support/stub_record_helper.rb @@ -0,0 +1,41 @@ +module StubRecordHelper + + # Will expect find_by_param or find_by_id to be called on klass and + # return the record given. + # If no record is given but a hash or nil will create a stub based on + # that instead and returns the stub. + def find_record(klass, record_or_method_hash = {}) + record = stub_record(klass, record_or_method_hash) + finder = klass.respond_to?(:find_by_param) ? :find_by_param : :find_by_id + klass.expects(finder).with(record.to_param).returns(record) + return record + end + + # Create a stub that has the usual functions of a database record. + # It won't fail on rendering a form for example. + # + # If the second parameter is a record we return the record itself. + # This way you can build functions that either take a record or a + # method hash to stub from. See find_record for an example. + def stub_record(klass, record_or_method_hash = {}, persisted = true) + if record_or_method_hash && !record_or_method_hash.is_a?(Hash) + return record_or_method_hash + end + stub record_params_for(klass, record_or_method_hash, persisted) + end + + def record_params_for(klass, params = {}, persisted = true) + if klass.respond_to?(:valid_attributes_hash) + params.reverse_merge!(klass.valid_attributes_hash) + end + params[:params] = params.stringify_keys + params.reverse_merge! :id => "A123", + :to_param => "A123", + :class => klass, + :to_key => ['123'], + :to_json => %Q({"stub":"#{klass.name}"}), + :new_record? => !persisted, + :persisted? => persisted + end + +end diff --git a/users/test/test_helper.rb b/users/test/test_helper.rb index 08d4d41..52dff53 100644 --- a/users/test/test_helper.rb +++ b/users/test/test_helper.rb @@ -1,10 +1,9 @@ ENV["RAILS_ENV"] = "test" require File.expand_path('../../../test/dummy/config/environment', __FILE__) require 'rails/test_help' -require 'mocha' +require 'mocha/setup' Rails.backtrace_cleaner.remove_silencers! # Load support files Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } - diff --git a/users/test/unit/user_test.rb b/users/test/unit/user_test.rb index 822ef33..cce11c2 100644 --- a/users/test/unit/user_test.rb +++ b/users/test/unit/user_test.rb @@ -5,6 +5,7 @@ class UserTest < ActiveSupport::TestCase include SRP::Util setup do @attribs = User.valid_attributes_hash + User.find_by_login(@attribs[:login]).try(:destroy) @user = User.new(@attribs) end @@ -19,18 +20,18 @@ class UserTest < ActiveSupport::TestCase end test "test require alphanumerical for login" do - @user.login = "qwär" + @user.login = "qw#r" assert !@user.valid? end - test "find_by_param gets User by login" do + test "find_by_param gets User by id" do @user.save - assert_equal @user, User.find_by_param(@user.login) + assert_equal @user, User.find_by_param(@user.id) @user.destroy end - test "to_param gives user login" do - assert_equal @user.login, @user.to_param + test "to_param gives user id" do + assert_equal @user.id, @user.to_param end test "verifier returns number for the hex in password_verifier" do diff --git a/users/test/unit/warden_strategy_secure_remote_password_test.rb b/users/test/unit/warden_strategy_secure_remote_password_test.rb new file mode 100644 index 0000000..319809a --- /dev/null +++ b/users/test/unit/warden_strategy_secure_remote_password_test.rb @@ -0,0 +1,63 @@ +class WardenStrategySecureRemotePasswordTest < ActiveSupport::TestCase + +# TODO : turn this into sth. real +=begin + setup do + @user = stub :login => "me", :id => 123 + @client_hex = 'a123' + @client_rnd = @client_hex.hex + @server_hex = 'b123' + @server_rnd = @server_hex.hex + @server_rnd_exp = 'e123'.hex + @salt = 'stub user salt' + @server_handshake = stub :aa => @client_rnd, :bb => @server_rnd, :b => @server_rnd_exp + @server_auth = 'adfe' + end + + + test "should perform handshake" do + @user.expects(:initialize_auth). + with(@client_rnd). + returns(@server_handshake) + @server_handshake.expects(:to_json). + returns({'B' => @server_hex, 'salt' => @salt}.to_json) + User.expects(:find_by_param).with(@user.login).returns(@user) + assert_equal @server_handshake, session[:handshake] + assert_response :success + assert_json_response :B => @server_hex, :salt => @salt + end + + test "should report user not found" do + unknown = "login_that_does_not_exist" + User.expects(:find_by_param).with(unknown).raises(RECORD_NOT_FOUND) + post :create, :login => unknown + assert_response :success + assert_json_error "login" => ["unknown user"] + end + + test "should authorize" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + returns(@user) + @server_handshake.expects(:to_json). + returns({:M2 => @server_auth}.to_json) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_json_response :M2 => @server_auth + assert_equal @user.id, session[:user_id] + end + + test "should report wrong password" do + session[:handshake] = @server_handshake + @server_handshake.expects(:authenticate!). + with(@client_rnd). + raises(WRONG_PASSWORD) + post :update, :id => @user.login, :client_auth => @client_hex + assert_nil session[:handshake] + assert_nil session[:user_id] + assert_json_error "password" => ["wrong password"] + end + +=end +end |