diff options
| author | Azul <azul@leap.se> | 2012-12-07 08:28:23 +0100 | 
|---|---|---|
| committer | Azul <azul@leap.se> | 2012-12-07 08:28:23 +0100 | 
| commit | 1ec55c4f562a4fdd57c50077ff286ef08e9978a1 (patch) | |
| tree | 16203d2ca4f32e24d38fef6062aa9534cecb3bfe | |
| parent | effa6b0f84cfe954cc9dd73f592663b743b0d857 (diff) | |
| parent | a3dce077881c7e97090e5e560b1fb004952d5b23 (diff) | |
Merge branch 'develop'
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 | 
