summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--CUSTOM.md13
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock287
-rw-r--r--INSTALL.md4
-rw-r--r--README.md2
-rw-r--r--Rakefile2
-rw-r--r--app/assets/javascripts/application.js3
-rw-r--r--app/assets/stylesheets/leap.scss85
-rw-r--r--app/controllers/application_controller.rb6
-rw-r--r--app/controllers/home_controller.rb2
-rw-r--r--app/helpers/application_helper.rb23
-rw-r--r--app/views/home/index.html.haml4
-rw-r--r--app/views/layouts/_navigation.html.haml4
-rw-r--r--billing/Gemfile2
-rw-r--r--billing/app/controllers/billing_admin_controller.rb29
-rw-r--r--billing/app/controllers/subscriptions_controller.rb19
-rw-r--r--billing/app/helpers/billing_helper.rb29
-rw-r--r--billing/app/models/customer.rb6
-rw-r--r--billing/app/views/billing_admin/show.html.haml7
-rw-r--r--billing/app/views/customer/show.html.haml2
-rw-r--r--billing/app/views/subscriptions/_subscription_details.html.haml11
-rw-r--r--billing/app/views/subscriptions/index.html.haml8
-rw-r--r--billing/app/views/subscriptions/show.html.haml2
-rw-r--r--billing/config/locales/en.yml3
-rw-r--r--billing/config/routes.rb1
-rw-r--r--billing/test/functional/customers_controller_test.rb8
-rw-r--r--billing/test/functional/subscriptions_controller_test.rb (renamed from billing/test/functional/subsciptions_controller_test.rb)0
-rw-r--r--billing/test/integration/subscription_test.rb12
-rw-r--r--certs/Gemfile2
-rw-r--r--common_dependencies.rb2
-rw-r--r--config/application.rb8
-rw-r--r--config/customization/README.md27
-rw-r--r--config/defaults.yml56
-rw-r--r--config/deploy.rb.example2
-rw-r--r--config/initializers/customization.rb36
-rw-r--r--core/Gemfile2
-rw-r--r--core/app/assets/javascripts/platform.js92
-rw-r--r--core/app/helpers/core_helper.rb2
-rw-r--r--core/app/helpers/download_helper.rb33
-rw-r--r--core/app/views/common/_download_for_os.html.haml16
-rw-r--r--core/app/views/common/_home_page_buttons.html.haml13
-rw-r--r--core/config/locales/en.yml12
-rw-r--r--core/leap_web_core.gemspec2
-rw-r--r--core/lib/extensions/couchrest.rb45
-rw-r--r--core/lib/tasks/leap_web_core_tasks.rake29
-rw-r--r--help/Gemfile2
-rw-r--r--help/app/assets/javascripts/tickets.js2
-rw-r--r--help/app/controllers/tickets_controller.rb11
-rw-r--r--help/app/models/account_extension/tickets.rb13
-rw-r--r--help/app/models/ticket.rb11
-rw-r--r--help/app/views/tickets/_edit_form.html.haml6
-rw-r--r--help/app/views/tickets/_ticket.html.haml2
-rw-r--r--help/app/views/tickets/new.html.haml2
-rw-r--r--help/config/initializers/account_lifecycle.rb3
-rw-r--r--help/test/factories.rb14
-rw-r--r--help/test/functional/tickets_controller_test.rb4
-rw-r--r--help/test/unit/account_extension_test.rb12
-rw-r--r--help/test/unit/ticket_comment_test.rb2
-rw-r--r--help/test/unit/ticket_test.rb48
-rw-r--r--lib/leap_web/version.rb2
-rw-r--r--public/leap-img/128/mask.pngbin0 -> 3654 bytes
-rw-r--r--test/integration/os_detection_test.rb24
-rw-r--r--users/Gemfile2
m---------users/app/assets/javascripts/srp0
-rw-r--r--users/app/assets/javascripts/users.js51
-rw-r--r--users/app/controllers/keys_controller.rb18
-rw-r--r--users/app/controllers/overviews_controller.rb9
-rw-r--r--users/app/controllers/sessions_controller.rb5
-rw-r--r--users/app/controllers/users_controller.rb20
-rw-r--r--users/app/controllers/v1/users_controller.rb8
-rw-r--r--users/app/models/account.rb25
-rw-r--r--users/app/models/identity.rb42
-rw-r--r--users/app/models/local_email.rb33
-rw-r--r--users/app/models/pgp_key.rb48
-rw-r--r--users/app/models/service_level.rb19
-rw-r--r--users/app/models/token.rb36
-rw-r--r--users/app/models/unauthenticated_user.rb2
-rw-r--r--users/app/models/user.rb24
-rw-r--r--users/app/views/overviews/show.html.haml22
-rw-r--r--users/app/views/users/_change_password.html.haml21
-rw-r--r--users/app/views/users/_change_pgp_key.html.haml13
-rw-r--r--users/app/views/users/_change_service_level.html.haml18
-rw-r--r--users/app/views/users/_destroy_account.html.haml27
-rw-r--r--users/app/views/users/_edit.html.haml74
-rw-r--r--users/app/views/users/_user.html.haml2
-rw-r--r--users/app/views/users/show.html.haml23
-rw-r--r--users/config/locales/en.yml1
-rw-r--r--users/config/routes.rb5
-rw-r--r--users/test/factories.rb12
-rw-r--r--users/test/functional/keys_controller_test.rb32
-rw-r--r--users/test/functional/sessions_controller_test.rb21
-rw-r--r--users/test/functional/users_controller_test.rb9
-rw-r--r--users/test/functional/v1/sessions_controller_test.rb19
-rw-r--r--users/test/integration/api/account_flow_test.rb34
-rwxr-xr-xusers/test/integration/api/python/umlauts.py79
-rw-r--r--users/test/integration/browser/account_test.rb114
-rw-r--r--users/test/support/auth_test_helper.rb14
-rw-r--r--users/test/support/integration_test_helper.rb6
-rw-r--r--users/test/unit/account_test.rb7
-rw-r--r--users/test/unit/identity_test.rb29
-rw-r--r--users/test/unit/local_email_test.rb31
-rw-r--r--users/test/unit/token_test.rb23
103 files changed, 1732 insertions, 360 deletions
diff --git a/.gitignore b/.gitignore
index 84acd8d..f65233f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,6 @@
/pkg
/*/pkg
/log
-Gemfile.lock
*/Gemfile.lock
test/dummy/log/*
test/dummy/tmp/*
diff --git a/CUSTOM.md b/CUSTOM.md
index 67fdac0..8671323 100644
--- a/CUSTOM.md
+++ b/CUSTOM.md
@@ -1,11 +1,14 @@
-# Customization #
+Customization
+==============================
-Leap Web is based on Engines. All things in `app` will overwrite the default behaviour. You can either create a new rails app and include the leap_web gem or clone the leap web repository and add your customizations to the `app` directory.
+Customization directory
+---------------------------------------
-## CSS Customization ##
+See config/customization/README.md
-We use scss. It's a superset of css3. Add your customizations to `app/assets/stylesheets`.
+Engines
+---------------------
-## Disabling an Engine ##
+Leap Web is based on Engines. All things in `app` will overwrite the default behaviour. You can either create a new rails app and include the leap_web gem or clone the leap web repository and add your customizations to the `app` directory.
If you have no use for one of the engines you can remove it from the Gemfile. Not however that your app might still need to provide some functionality for the other engines to work. For example the users engine provides `current_user` and other methods.
diff --git a/Gemfile b/Gemfile
index 8ca0eaf..a346afa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -29,4 +29,4 @@ group :test do
end
# unreleased so far ... but leap_web_certs need it
-gem 'certificate_authority', :git => 'git://github.com/cchandler/certificate_authority.git'
+gem 'certificate_authority', :git => 'https://github.com/cchandler/certificate_authority.git'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..918fdba
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,287 @@
+GIT
+ remote: https://github.com/cchandler/certificate_authority.git
+ revision: 58161e4552cc1aeca846da3e25ed66721354ee11
+ specs:
+ certificate_authority (0.2.0)
+ activemodel (>= 3.0.6)
+
+PATH
+ remote: billing
+ specs:
+ leap_web_billing (0.2.8)
+ braintree
+ leap_web_core (= 0.2.8)
+
+PATH
+ remote: certs
+ specs:
+ leap_web_certs (0.2.8)
+ certificate_authority (>= 0.2.0)
+ leap_web_core (= 0.2.8)
+
+PATH
+ remote: core
+ specs:
+ leap_web_core (0.2.8)
+ couchrest (~> 1.1.3)
+ couchrest_model (~> 2.0.0)
+ couchrest_session_store (~> 0.2.4)
+ json
+ rails (~> 3.2.11)
+
+PATH
+ remote: help
+ specs:
+ leap_web_help (0.2.8)
+ leap_web_core (= 0.2.8)
+
+PATH
+ remote: users
+ specs:
+ leap_web_users (0.2.8)
+ leap_web_core (= 0.2.8)
+ rails_warden
+ ruby-srp (~> 0.2.1)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ SyslogLogger (2.0)
+ actionmailer (3.2.15)
+ actionpack (= 3.2.15)
+ mail (~> 2.5.4)
+ actionpack (3.2.15)
+ activemodel (= 3.2.15)
+ activesupport (= 3.2.15)
+ builder (~> 3.0.0)
+ erubis (~> 2.7.0)
+ journey (~> 1.0.4)
+ rack (~> 1.4.5)
+ rack-cache (~> 1.2)
+ rack-test (~> 0.6.1)
+ sprockets (~> 2.2.1)
+ activemodel (3.2.15)
+ activesupport (= 3.2.15)
+ builder (~> 3.0.0)
+ activerecord (3.2.15)
+ activemodel (= 3.2.15)
+ activesupport (= 3.2.15)
+ arel (~> 3.0.2)
+ tzinfo (~> 0.3.29)
+ activeresource (3.2.15)
+ activemodel (= 3.2.15)
+ activesupport (= 3.2.15)
+ activesupport (3.2.15)
+ i18n (~> 0.6, >= 0.6.4)
+ multi_json (~> 1.0)
+ addressable (2.3.5)
+ arel (3.0.2)
+ bootstrap-sass (2.1.1.0)
+ bootswatch-rails (0.5.0)
+ railties (>= 3.1)
+ braintree (2.25.0)
+ builder (>= 2.0.0)
+ builder (3.0.4)
+ capybara (2.1.0)
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ client_side_validations (3.2.6)
+ client_side_validations-simple_form (2.1.0)
+ client_side_validations (~> 3.2.5)
+ simple_form (~> 2.1.0)
+ cliver (0.2.2)
+ 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.6.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.1)
+ activemodel (>= 3.0)
+ couchrest (~> 1.1.3)
+ mime-types (>= 1.15)
+ tzinfo (>= 0.3.22)
+ couchrest_session_store (0.2.4)
+ actionpack
+ couchrest
+ couchrest_model
+ daemons (1.1.9)
+ debugger (1.6.2)
+ columnize (>= 0.3.1)
+ debugger-linecache (~> 1.2.0)
+ debugger-ruby_core_source (~> 1.2.3)
+ debugger-linecache (1.2.0)
+ debugger-ruby_core_source (1.2.4)
+ erubis (2.7.0)
+ eventmachine (1.0.3)
+ execjs (2.0.2)
+ factory_girl (4.2.0)
+ activesupport (>= 3.0.0)
+ factory_girl_rails (4.2.1)
+ factory_girl (~> 4.2.0)
+ railties (>= 3.0.0)
+ fake_braintree (0.4)
+ activesupport
+ braintree (~> 2.5)
+ capybara
+ i18n
+ sinatra
+ thin
+ faker (1.2.0)
+ i18n (~> 0.5)
+ haml (3.1.8)
+ 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.3)
+ i18n (0.6.9)
+ journey (1.0.4)
+ jquery-rails (3.0.4)
+ railties (>= 3.0, < 5.0)
+ thor (>= 0.14, < 2.0)
+ json (1.8.1)
+ kaminari (0.13.0)
+ actionpack (>= 3.0.0)
+ activesupport (>= 3.0.0)
+ railties (>= 3.0.0)
+ launchy (2.3.0)
+ addressable (~> 2.3)
+ libv8 (3.3.10.4)
+ mail (2.5.4)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ metaclass (0.0.1)
+ mime-types (1.25.1)
+ mini_portile (0.5.1)
+ mocha (0.13.3)
+ metaclass (~> 0.0.1)
+ multi_json (1.8.2)
+ nokogiri (1.6.0)
+ mini_portile (~> 0.5.0)
+ poltergeist (1.4.1)
+ capybara (~> 2.1.0)
+ cliver (~> 0.2.1)
+ multi_json (~> 1.0)
+ websocket-driver (>= 0.2.0)
+ polyglot (0.3.3)
+ quiet_assets (1.0.2)
+ railties (>= 3.1, < 5.0)
+ rack (1.4.5)
+ rack-cache (1.2)
+ rack (>= 0.4)
+ rack-protection (1.5.0)
+ rack
+ rack-ssl (1.3.3)
+ rack
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rails (3.2.15)
+ actionmailer (= 3.2.15)
+ actionpack (= 3.2.15)
+ activerecord (= 3.2.15)
+ activeresource (= 3.2.15)
+ activesupport (= 3.2.15)
+ bundler (~> 1.0)
+ railties (= 3.2.15)
+ rails-i18n (3.0.0)
+ i18n (~> 0.5)
+ rails (>= 3.0.0, < 4.0.0)
+ rails_warden (0.5.7)
+ warden (>= 1.0.0)
+ railties (3.2.15)
+ actionpack (= 3.2.15)
+ activesupport (= 3.2.15)
+ rack-ssl (~> 1.3.2)
+ rake (>= 0.8.7)
+ rdoc (~> 3.4)
+ thor (>= 0.14.6, < 2.0)
+ rake (10.1.0)
+ rdoc (3.12.2)
+ json (~> 1.4)
+ rest-client (1.6.7)
+ mime-types (>= 1.16)
+ ruby-srp (0.2.1)
+ sass (3.2.12)
+ sass-rails (3.2.6)
+ railties (~> 3.2.0)
+ sass (>= 3.1.10)
+ tilt (~> 1.3)
+ simple_form (2.1.0)
+ actionpack (~> 3.0)
+ activemodel (~> 3.0)
+ sinatra (1.4.3)
+ rack (~> 1.4)
+ rack-protection (~> 1.4)
+ tilt (~> 1.3, >= 1.3.4)
+ sprockets (2.2.2)
+ hike (~> 1.2)
+ multi_json (~> 1.0)
+ rack (~> 1.0)
+ tilt (~> 1.1, != 1.3.0)
+ therubyracer (0.10.2)
+ libv8 (~> 3.3.10)
+ thin (1.5.1)
+ daemons (>= 1.0.9)
+ eventmachine (>= 0.12.6)
+ rack (>= 1.0.0)
+ thor (0.18.1)
+ tilt (1.4.1)
+ treetop (1.4.15)
+ polyglot
+ polyglot (>= 0.3.1)
+ tzinfo (0.3.38)
+ uglifier (1.2.7)
+ execjs (>= 0.3.0)
+ multi_json (~> 1.3)
+ warden (1.2.3)
+ rack (>= 1.0)
+ websocket-driver (0.3.0)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ SyslogLogger (~> 2.0)
+ bootstrap-sass (~> 2.1.0)
+ bootswatch-rails (~> 0.5.0)
+ capybara
+ certificate_authority!
+ client_side_validations
+ client_side_validations-simple_form
+ coffee-rails (~> 3.2.2)
+ debugger
+ factory_girl_rails
+ fake_braintree
+ faker
+ haml (~> 3.1.7)
+ haml-rails (~> 0.3.4)
+ jquery-rails
+ kaminari (= 0.13.0)
+ launchy
+ leap_web_billing!
+ leap_web_certs!
+ leap_web_core!
+ leap_web_help!
+ leap_web_users!
+ mocha (~> 0.13.0)
+ poltergeist
+ quiet_assets
+ rails-i18n
+ sass-rails (~> 3.2.5)
+ simple_form
+ therubyracer (~> 0.10.2)
+ thin
+ uglifier (~> 1.2.7)
diff --git a/INSTALL.md b/INSTALL.md
index 7e95b76..75cb2a6 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -8,7 +8,7 @@ Install git, ruby 1.9, rubygems and couchdb on your system. Then run
```
gem install bundler
-git clone git://github.com/leapcode/leap_web.git
+git clone https://leap.se/git/leap_web
cd leap_web
git submodule init
git submodule update
@@ -36,7 +36,7 @@ The following packages need to be installed:
Simply clone the git repository:
```
- git clone git://github.com/leapcode/leap_web.git
+ git clone https://leap.se/git/leap_web
cd leap_web
```
diff --git a/README.md b/README.md
index cfdad33..1d6016c 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ Typically, this application is installed automatically as part of the LEAP Platf
### Install system requirements
- sudo apt-get install git ruby1.8 rubygems1.8 couchdb
+ sudo apt-get install git ruby1.9.3 rubygems 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``.
diff --git a/Rakefile b/Rakefile
index 8b58316..47b6c3f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,6 +2,8 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+RAKE=true # let environment initialization code know if we are running via rake or not.
+
require 'rake/packagetask'
require 'rubygems/package_task'
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index cd90934..03a40da 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -16,9 +16,8 @@
//= require bootstrap
//= require rails.validations
//= require rails.validations.simple_form
-
//= require leap
+//= require platform
//= require tickets
//= require users
-
//= require_tree .
diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss
index b382773..abfea05 100644
--- a/app/assets/stylesheets/leap.scss
+++ b/app/assets/stylesheets/leap.scss
@@ -43,25 +43,95 @@
}
//
+// OS specific
+//
+
+.os-android {
+ display: none !important;
+}
+
+html.android .os-android {
+ display: inherit !important;
+}
+
+.os-linux32 {
+ display: none !important;
+}
+
+html.linux32 .os-linux32 {
+ display: inherit !important;
+}
+
+.os-linux64 {
+ display: none !important;
+}
+
+html.linux64 .os-linux64 {
+ display: inherit !important;
+}
+
+.os-windows {
+ display: none !important;
+}
+
+html.windows .os-windows {
+ display: inherit !important;
+}
+
+.os-osx {
+ display: none !important;
+}
+
+html.osx .os-osx {
+ display: inherit !important;
+}
+
+.os-other {
+ display: none !important;
+}
+
+html.oldmac, html.oldwin, html.ios, html.fxos, html.other {
+ .os-other {
+ display: inherit !important;
+ }
+}
+
+//
// ICONS
//
-[class^="big-icon-"],
-[class*=" big-icon-"] {
+[class*="-icon-"] {
display: inline-block;
- width: 32px;
- height: 32px;
@include ie7-restore-right-whitespace();
- line-height: 32px;
vertical-align: middle;
background-repeat: no-repeat;
margin-top: 1px;
}
+[class^="big-icon-"],
+[class*=" big-icon-"] {
+ width: 32px;
+ height: 32px;
+ line-height: 32px;
+}
+
+[class^="huge-icon-"],
+[class*=" huge-icon-"] {
+ width: 128px;
+ height: 128px;
+ line-height: 128px;
+}
+
+
.big-icon-arrow-down {
background-image: url(/leap-img/32/arrow-down.png)
}
+.huge-icon-mask {
+ height: 64px;
+ background-image: url(/leap-img/128/mask.png)
+}
+
//
// TYPOGRAPHY
//
@@ -152,6 +222,9 @@ input, textarea {
.download {
a.btn {
width: 14em;
+ small {
+ font-weight: normal;
+ }
}
}
a.btn {
@@ -191,4 +264,4 @@ input, textarea {
.overview li {
padding: 6px 0;
-} \ No newline at end of file
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index b808e1c..de8d06b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -10,12 +10,14 @@ class ApplicationController < ActionController::Base
rescue_from StandardError do |e|
respond_to do |format|
- format.json { render_json_error }
+ format.json { render_json_error(e) }
format.all { raise e } # reraise the exception so the normal thing happens.
end
end
- def render_json_error
+ def render_json_error(e)
+ Rails.logger.error e
+ Rails.logger.error e.backtrace.join("\n")
render status: 500,
json: {error: "The server failed to process your request. We'll look into it."}
end
diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb
index be26eb6..1d62178 100644
--- a/app/controllers/home_controller.rb
+++ b/app/controllers/home_controller.rb
@@ -3,7 +3,7 @@ class HomeController < ApplicationController
def index
if logged_in?
- redirect_to user_overview_url(current_user)
+ redirect_to current_user
end
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 1e79990..90e649a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -19,18 +19,25 @@ module ApplicationHelper
# http://twitter.github.io/bootstrap/base-css.html#icons
#
def icon(name, color=nil)
+ "<i class=\"icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ end
+
+ def big_icon(name, color=nil)
+ "<i class=\"big-icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ end
+
+ def huge_icon(name, color=nil)
+ "<i class=\"huge-icon-#{name} #{color_class(color)}\"></i> ".html_safe
+ end
+
+ def color_class(color)
if color.nil?
- color_class = nil
+ nil
elsif color == :black
- color_class = 'icon-black'
+ 'icon-black'
elsif color == :white
- color_class = 'icon-white'
+ 'icon-white'
end
- "<i class=\"icon-#{name} #{color_class}\"></i> ".html_safe
- end
-
- def big_icon(name, color=nil)
- "<i class=\"big-icon-#{name}\"></i> ".html_safe
end
def format_flash(msg)
diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml
index 79b515e..67b2c06 100644
--- a/app/views/home/index.html.haml
+++ b/app/views/home/index.html.haml
@@ -6,6 +6,10 @@
%p
We provide secure communication services, including encrypted internet, email (coming soon), and chat (coming later).
+ .row-fluid
+ .span6.offset3
+ = render 'layouts/messages'
+ .row-fluid
= home_page_buttons
- if Rails.env == 'development'
diff --git a/app/views/layouts/_navigation.html.haml b/app/views/layouts/_navigation.html.haml
index b89655f..6de567a 100644
--- a/app/views/layouts/_navigation.html.haml
+++ b/app/views/layouts/_navigation.html.haml
@@ -1,7 +1,7 @@
%ul.nav.sidenav
- = link_to_navigation t(:overview), user_overview_path(@user), :active => controller?(:overviews)
+ = link_to_navigation t(:overview), @user, :active => controller?(:overviews)
= link_to_navigation t(:account_settings), edit_user_path(@user), :active => controller?(:users)
- # will want link for identity settings
= link_to_navigation t(:support_tickets), auto_tickets_path, :active => controller?(:tickets)
- = link_to_navigation t(:billing_settings), show_or_new_customer_link(@user), :active => controller?(:customer, :payments, :subscriptions, :credit_card_info) if APP_CONFIG[:payment].present?
+ = link_to_navigation t(:billing_settings), billing_top_link(@user), :active => controller?(:customer, :payments, :subscriptions, :credit_card_info) if APP_CONFIG[:payment].present?
= link_to_navigation t(:logout), logout_path, :method => :delete
diff --git a/billing/Gemfile b/billing/Gemfile
index 68ea51b..30e9669 100644
--- a/billing/Gemfile
+++ b/billing/Gemfile
@@ -1,4 +1,4 @@
-source "http://rubygems.org"
+source "https://rubygems.org"
eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb'))
eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb'))
diff --git a/billing/app/controllers/billing_admin_controller.rb b/billing/app/controllers/billing_admin_controller.rb
new file mode 100644
index 0000000..cd6149f
--- /dev/null
+++ b/billing/app/controllers/billing_admin_controller.rb
@@ -0,0 +1,29 @@
+class BillingAdminController < BillingBaseController
+ before_filter :authorize_admin
+
+ def show
+
+ br_atleast_90_days = Braintree::Subscription.search do |search|
+ search.days_past_due >= 90
+ end
+ @past_due_atleast_90_days = braintree_resource_collection_to_array(br_atleast_90_days)
+
+ br_all_past_due = Braintree::Subscription.search do |search|
+ search.status.is Braintree::Subscription::Status::PastDue
+ #cannot search by balance.
+ end
+ @all_past_due = braintree_resource_collection_to_array(br_all_past_due)
+
+ end
+
+ private
+
+ def braintree_resource_collection_to_array(braintree_resource_collection)
+ array = []
+ braintree_resource_collection.each do |object|
+ array << object
+ end
+ array
+ end
+
+end
diff --git a/billing/app/controllers/subscriptions_controller.rb b/billing/app/controllers/subscriptions_controller.rb
index 7689f35..01aaab4 100644
--- a/billing/app/controllers/subscriptions_controller.rb
+++ b/billing/app/controllers/subscriptions_controller.rb
@@ -1,7 +1,9 @@
class SubscriptionsController < BillingBaseController
before_filter :authorize
before_filter :fetch_subscription, :only => [:show, :destroy]
- before_filter :confirm_no_active_subscription, :only => [:new, :create]
+ before_filter :confirm_cancel_subscription, :only => [:destroy]
+ before_filter :confirm_self_or_admin, :only => [:index]
+ before_filter :confirm_no_pending_active_pastdue_subscription, :only => [:new, :create]
# for now, admins cannot create or destroy subscriptions for others:
before_filter :confirm_self, :only => [:new, :create]
@@ -16,6 +18,7 @@ class SubscriptionsController < BillingBaseController
def create
@result = Braintree::Subscription.create( :payment_method_token => params[:payment_method_token], :plan_id => params[:plan_id] )
+ #if you want to test pastdue, can add :price => '2001', :trial_period => true,:trial_duration => 1,:trial_duration_unit => "day" and then wait a day
end
def destroy
@@ -38,10 +41,14 @@ class SubscriptionsController < BillingBaseController
end
- def confirm_no_active_subscription
+ def confirm_cancel_subscription
+ access_denied unless view_context.allow_cancel_subscription(@subscription)
+ end
+
+ def confirm_no_pending_active_pastdue_subscription
@customer = Customer.find_by_user_id(@user.id)
- if subscription = @customer.subscriptions # will return active subscription, if it exists
- redirect_to subscription_path(subscription.id), :notice => 'You already have an active subscription'
+ if subscription = @customer.subscriptions # will return pending, active or pastdue subscription, if it exists
+ redirect_to user_subscription_path(@user, subscription.id), :notice => 'You already have a subscription'
end
end
@@ -49,4 +56,8 @@ class SubscriptionsController < BillingBaseController
@user == current_user
end
+ def confirm_self_or_admin
+ access_denied unless confirm_self or admin?
+ end
+
end
diff --git a/billing/app/helpers/billing_helper.rb b/billing/app/helpers/billing_helper.rb
index 3c0691f..b9e5e2e 100644
--- a/billing/app/helpers/billing_helper.rb
+++ b/billing/app/helpers/billing_helper.rb
@@ -9,6 +9,15 @@ module BillingHelper
form_for object, options, &block
end
+ def billing_top_link(user)
+ # for admins, top link will show special admin information, which has link to show their own customer information
+ if (admin? and user == current_user)
+ billing_admin_path
+ else
+ show_or_new_customer_link(user)
+ end
+ end
+
def show_or_new_customer_link(user)
# Link to show if user is admin viewing another user, or user is already a customer.
# Otherwise link to create a new customer.
@@ -19,4 +28,24 @@ module BillingHelper
end
end
+ # a bit strange to put here, but we don't have a subscription model
+ def user_for_subscription(subscription)
+
+ if (transaction = subscription.transactions.first)
+ # much quicker, but will only work if there is already a transaction associated with subscription (should generally be)
+ braintree_customer_id = transaction.customer_details.id
+ else
+ credit_card = Braintree::CreditCard.find(subscription.payment_method_token)
+ braintree_customer_id = credit_card.customer_id
+ end
+
+ customer = Customer.find_by_braintree_customer_id(braintree_customer_id)
+ user = User.find(customer.user_id)
+
+ end
+
+ def allow_cancel_subscription(subscription)
+ ['Active', 'Pending'].include? subscription.status or (admin? and subscription.status == 'Past Due')
+ end
+
end
diff --git a/billing/app/models/customer.rb b/billing/app/models/customer.rb
index f01c300..1acc7a5 100644
--- a/billing/app/models/customer.rb
+++ b/billing/app/models/customer.rb
@@ -40,19 +40,19 @@ class Customer < CouchRest::Model::Base
end
# based on 2nd parameter, either returns the single active subscription (or nil if there isn't one), or an array of all subsciptions
- def subscriptions(braintree_data=nil, only_active=true)
+ def subscriptions(braintree_data=nil, only_pending_active_pastdue=true)
self.with_braintree_data!
return unless has_payment_info?
subscriptions = []
self.default_credit_card.subscriptions.each do |sub|
- if only_active and sub.status == 'Active'
+ if only_pending_active_pastdue and ['Pending', 'Active','Past Due'].include? sub.status
return sub
else
subscriptions << sub
end
end
- only_active ? nil : subscriptions
+ only_pending_active_pastdue ? nil : subscriptions
end
end
diff --git a/billing/app/views/billing_admin/show.html.haml b/billing/app/views/billing_admin/show.html.haml
new file mode 100644
index 0000000..0382cf0
--- /dev/null
+++ b/billing/app/views/billing_admin/show.html.haml
@@ -0,0 +1,7 @@
+%legend= t(:more_than_90_days_past_due)
+= render(:partial => "subscriptions/subscription_details", :collection => @past_due_atleast_90_days, :as => 'subscription', :locals => {:show_user => true}) || t(:none)
+%legend= t(:all_past_due)
+= render(:partial => "subscriptions/subscription_details", :collection => @all_past_due, :as => 'subscription', :locals => {:show_user => true}) || t(:none)
+
+%legend= t(:your_settings)
+= link_to 'view own billing settings', show_or_new_customer_link(current_user) \ No newline at end of file
diff --git a/billing/app/views/customer/show.html.haml b/billing/app/views/customer/show.html.haml
index d91a4e7..ec1779c 100644
--- a/billing/app/views/customer/show.html.haml
+++ b/billing/app/views/customer/show.html.haml
@@ -15,7 +15,7 @@
= render :partial => "subscriptions/subscription_details", :locals => {:subscription => @active_subscription}
- else
%p
- = t(:no_active_subscription)
+ = t(:no_relevant_subscription)
- if current_user == @user
%p
.form-actions
diff --git a/billing/app/views/subscriptions/_subscription_details.html.haml b/billing/app/views/subscriptions/_subscription_details.html.haml
index 6eda7ca..6145c95 100644
--- a/billing/app/views/subscriptions/_subscription_details.html.haml
+++ b/billing/app/views/subscriptions/_subscription_details.html.haml
@@ -1,7 +1,14 @@
%p
+ - if local_assigns[:show_user]
+ User:
+ - user_to_show = user_for_subscription(subscription)
+ = link_to user_to_show.login, user_overview_path(user_to_show)
+ ID:
= link_to subscription.id, user_subscription_path(@user, subscription.id)
Balance:
- = number_to_currency(subscription.balance)
+ - color = (subscription.balance > 0) ? "red" : ""
+ %font{:color => color}
+ = number_to_currency(subscription.balance)
Bill on:
= subscription.billing_day_of_month
Start date:
@@ -11,7 +18,7 @@
Plan:
= subscription.plan_id
Price:
- = subscription.price
+ = number_to_currency(subscription.price)
- color = (subscription.status == 'Active') ? "green" : "red"
Status:
%font{:color => color}
diff --git a/billing/app/views/subscriptions/index.html.haml b/billing/app/views/subscriptions/index.html.haml
index 87771e5..3d4e8fd 100644
--- a/billing/app/views/subscriptions/index.html.haml
+++ b/billing/app/views/subscriptions/index.html.haml
@@ -1,8 +1,8 @@
%h2=t :all_subscriptions
-- active = false
+- pending_active_pastdue = false
- @subscriptions.each do |s|
- - if s.status == 'Active'
- - active = true
+ - if ['Pending', 'Active','Past Due'].include? s.status
+ - pending_active_pastdue = true
= render :partial => "subscription_details", :locals => {:subscription => s}
-- if !active and @user == current_user
+- if !pending_active_pastdue and @user == current_user
= link_to 'subscribe to plan', new_subscription_path, :class => :btn \ No newline at end of file
diff --git a/billing/app/views/subscriptions/show.html.haml b/billing/app/views/subscriptions/show.html.haml
index 39f4d1a..2699db9 100644
--- a/billing/app/views/subscriptions/show.html.haml
+++ b/billing/app/views/subscriptions/show.html.haml
@@ -3,4 +3,4 @@
Current
Subscription
= render :partial => "subscription_details", :locals => {:subscription => @subscription}
-= link_to t(:cancel_subscription), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if @subscription.status == 'Active' # permission check or should that just be on show?
+= link_to t(:cancel_subscription), user_subscription_path(@user, @subscription.id), :confirm => t(:are_you_sure), :method => :delete, :class => 'btn btn-danger' if allow_cancel_subscription(@subscription)
diff --git a/billing/config/locales/en.yml b/billing/config/locales/en.yml
index eda7da2..b418a17 100644
--- a/billing/config/locales/en.yml
+++ b/billing/config/locales/en.yml
@@ -3,4 +3,5 @@ en:
must_create_customer: "You must store a customer in braintree before subscribing to a plan"
subscribe: "Subscribe"
save_customer_info: "Save Customer Information"
- donation_not_payment: "Note: This is a donation, and will not be applied towards your account." \ No newline at end of file
+ donation_not_payment: "Note: This is a donation, and will not be applied towards your account."
+ no_relevant_subscription: "No subscription which is Active, Pending, or Past Due"
diff --git a/billing/config/routes.rb b/billing/config/routes.rb
index e024f43..dbdc24b 100644
--- a/billing/config/routes.rb
+++ b/billing/config/routes.rb
@@ -15,6 +15,7 @@ Rails.application.routes.draw do
match 'credit_card_info/confirm' => 'credit_card_info#confirm', :as => :confirm_credit_card_info
resources :subscriptions, :only => [:new, :create, :update] # index, show & destroy are within users path
+ match 'billing_admin' => 'billing_admin#show', :as => :billing_admin
#match 'transactions/:product_id/new' => 'transactions#new', :as => :new_transaction
#match 'transactions/confirm/:product_id' => 'transactions#confirm', :as => :confirm_transaction
diff --git a/billing/test/functional/customers_controller_test.rb b/billing/test/functional/customers_controller_test.rb
index d4881bf..46c33c9 100644
--- a/billing/test/functional/customers_controller_test.rb
+++ b/billing/test/functional/customers_controller_test.rb
@@ -7,10 +7,11 @@ class CustomersControllerTest < ActionController::TestCase
setup do
@user = FactoryGirl.create :user
@other_user = FactoryGirl.create :user
- FakeBraintree.clear!
- FakeBraintree.verify_all_cards!
+ #FakeBraintree.clear!
+ #FakeBraintree.verify_all_cards!
testid = 'testid'
- FakeBraintree::Customer.new({:credit_cards => [{:number=>"5105105105105100", :expiration_date=>"05/2013"}]}, {:id => testid, :merchant_id => Braintree::Configuration.merchant_id})
+ #this wasn't actually being used
+ #FakeBraintree::Customer.new({:credit_cards => [{:number=>"5105105105105100", :expiration_date=>"05/2013"}]}, {:id => testid, :merchant_id => Braintree::Configuration.merchant_id})
# any reason to call the create instance method on the FakeBraintree::Customer ?
@customer = Customer.new(:user_id => @other_user.id)
@customer.braintree_customer_id = testid
@@ -50,6 +51,7 @@ class CustomersControllerTest < ActionController::TestCase
test "show" do
+ skip "show customer"
login @other_user
# Below will fail, as when we go to fetch the customer data, Braintree::Customer.find(params[:id]) won't find the customer as it is a FakeBraintree customer.
#get :show, :id => @customer.braintree_customer_id
diff --git a/billing/test/functional/subsciptions_controller_test.rb b/billing/test/functional/subscriptions_controller_test.rb
index a6a1057..a6a1057 100644
--- a/billing/test/functional/subsciptions_controller_test.rb
+++ b/billing/test/functional/subscriptions_controller_test.rb
diff --git a/billing/test/integration/subscription_test.rb b/billing/test/integration/subscription_test.rb
index b893896..6356177 100644
--- a/billing/test/integration/subscription_test.rb
+++ b/billing/test/integration/subscription_test.rb
@@ -10,28 +10,34 @@ class SubscriptionTest < ActionDispatch::IntegrationTest
setup do
Warden.test_mode!
- @admin = stub_record :user, :admin => true
+ @admin = User.find_by_login('admin') || FactoryGirl.create(:user, login: 'admin')
@customer = stub_customer
@braintree_customer = @customer.braintree_customer
response = Braintree::Subscription.create plan_id: '5',
- payment_method_token: @braintree_customer.credit_cards.first.token
+ payment_method_token: @braintree_customer.credit_cards.first.token,
+ price: '10'
@subscription = response.subscription
Capybara.current_driver = Capybara.javascript_driver
end
teardown do
Warden.test_reset!
+ @admin.destroy
end
- test "admin can see subscription for another" do
+ test "admin can see all subscriptions for another" do
login_as @admin
@customer.stubs(:subscriptions).returns([@subscription])
+ @subscription.stubs(:balance).returns 0
visit user_subscriptions_path(@customer.user_id)
assert page.has_content?("Subscriptions")
assert page.has_content?("Status: Active")
page.save_screenshot('/tmp/subscriptions.png')
end
+ # test "user cannot see all subscriptions for other user" do
+ #end
+
#test "admin cannot add subscription for another" do
#end
diff --git a/certs/Gemfile b/certs/Gemfile
index 951d1b7..992f236 100644
--- a/certs/Gemfile
+++ b/certs/Gemfile
@@ -1,4 +1,4 @@
-source "http://rubygems.org"
+source "https://rubygems.org"
eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb'))
diff --git a/common_dependencies.rb b/common_dependencies.rb
index 6a43e26..2225613 100644
--- a/common_dependencies.rb
+++ b/common_dependencies.rb
@@ -1,5 +1,3 @@
-source "http://rubygems.org"
-
group :test do
# moching and stubing
gem 'mocha', '~> 0.13.0', :require => false
diff --git a/config/application.rb b/config/application.rb
index 8587ffc..2c9c55a 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -78,12 +78,18 @@ module LeapWeb
# Enable the asset pipeline
config.assets.enabled = true
- config.assets.initialize_on_precompile = false
+ config.assets.initialize_on_precompile = true # don't change this (see customization.rb)
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
# Set to false in order to see asset requests in the log
config.quiet_assets = true
+
+ ##
+ ## CUSTOMIZATION
+ ## see initializers/customization.rb
+ ##
+ config.paths['app/views'].unshift "config/customization/views"
end
end
diff --git a/config/customization/README.md b/config/customization/README.md
new file mode 100644
index 0000000..9c3e434
--- /dev/null
+++ b/config/customization/README.md
@@ -0,0 +1,27 @@
+Customizing LEAP Webapp
+============================================
+
+By default, this directory is empty. Any file you place here will override the default files for the application.
+
+For example:
+
+ stylesheets/ -- overrides files Rails.root/app/assets/stylesheets
+ tail.scss -- included before all others
+ head.scss -- included after all others
+
+ public/ -- overrides files in Rails.root/public
+ favicon.ico -- custom favicon
+ img/ -- customary directory to put images in
+
+ views/ -- overrides files Rails.root/app/views
+ home/
+ index.html.haml -- this file is what shows up on the home page
+
+ locales/ -- overrides files in Rails.root/config/locales
+ en.yml -- overrides for English
+ de.yml -- overrides for German
+ and so on...
+
+For most changes, the web application must be restarted after any changes are made to the customization directory.
+
+Sometimes a `rake tmp:clear` and a rails restart is required to pick up a new stylesheet.
diff --git a/config/defaults.yml b/config/defaults.yml
index 8d81668..260915e 100644
--- a/config/defaults.yml
+++ b/config/defaults.yml
@@ -13,34 +13,88 @@ cert_options: &cert_options
limited_cert_prefix: "LIMITED"
unlimited_cert_prefix: "UNLIMITED"
+downloads: &downloads
+ client_download_domain: https://downloads.leap.se
+ available_clients:
+ - linux32
+ - linux64
+ - osx
+ - windows
+ - android
+ download_paths:
+ android: /client/android/Bitmask-Android-latest.apk
+ linux: /client/linux
+ linux32: /client/linux/Bitmask-linux32-latest.tar.bz2
+ linux64: /client/linux/Bitmask-linux64-latest.tar.bz2
+ osx: /client/osx/Bitmask-OSX-latest.dmg
+ windows: /client/windows/Bitmask-win32-latest.zip
+ other: /client
+
common: &common
force_ssl: false
pagination_size: 30
auth:
token_expires_after: 60
+ # handles that will be blocked from being used as logins or email aliases
+ # in addition to the ones in /etc/passwd and http://tools.ietf.org/html/rfc2142
+ handle_blacklist: [certmaster, ssladmin, arin-admin, administrator, www-data, maildrop]
+ # handles that will be allowed despite being in /etc/passwd or rfc2142
+ handle_whitelist: []
+ # actions enabled in the account settings
+ # see /users/app/views/users/_edit.html.haml for a list.
+ user_actions: ['destroy_account']
+ admin_actions: ['change_pgp_key', 'change_service_level', 'destroy_account']
+
+service_levels: &service_levels
+ service_levels:
+ 0:
+ name: anonymous
+ cert_prefix: "LIMITED"
+ description: "anonymous account, with rate limited VPN"
+ 1:
+ name: free
+ cert_prefix: "LIMITED"
+ description: "free account, with rate limited VPN"
+ cost: 0
+ quota: 100
+ 2:
+ name: premium
+ cert_prefix: "UNLIMITED"
+ description: "premium account, with unlimited vpn"
+ cost:
+ USD: 10
+ EUR: 10
+ default_service_level: 1
development:
+ <<: *downloads
<<: *dev_ca
<<: *cert_options
<<: *common
+ <<: *service_levels
admins: [blue, admin, admin2]
domain: example.org
secret_token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
payment: []
+ reraise_errors: true
test:
+ <<: *downloads
<<: *dev_ca
<<: *cert_options
<<: *common
+ <<: *service_levels
admins: [admin, admin2]
domain: test.me
secret_token: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
payment: [billing]
+ reraise_errors: true
production:
+ <<: *downloads
<<: *cert_options
<<: *common
admins: []
domain: example.net
payment: []
-# logfile: /path/to/your/logs
+ # logfile: /path/to/your/logs
diff --git a/config/deploy.rb.example b/config/deploy.rb.example
index 9e54c22..1fd4b8c 100644
--- a/config/deploy.rb.example
+++ b/config/deploy.rb.example
@@ -3,7 +3,7 @@ require "bundler/capistrano"
set :application, "webapp"
set :scm, :git
-set :repository, "git://leap.se/leap_web"
+set :repository, "https://leap.se/git/leap_web"
set :branch, "master"
set :deploy_via, :remote_cache
diff --git a/config/initializers/customization.rb b/config/initializers/customization.rb
new file mode 100644
index 0000000..bc9c834
--- /dev/null
+++ b/config/initializers/customization.rb
@@ -0,0 +1,36 @@
+#
+# When deploying, common customizations can be dropped in config/customizations. This initializer makes this work.
+#
+customization_directory = "#{Rails.root}/config/customization"
+
+#
+# Set customization views as the first view path
+#
+# Rails.application.config.paths['app/views'].unshift "config/customization/views"
+# (For some reason, this does not work here. See application.rb for where this is actually called.)
+
+#
+# Set customization stylesheets as the first asset path
+#
+# Some notes:
+#
+# * This cannot go in application.rb, as far as I can tell. In application.rb, the default paths
+# haven't been loaded yet, so the path we add will always end up at the end unless we add it here.
+#
+# * For this to work, config.assets.initialize_on_precompile MUST be set to true, otherwise
+# this initializer will never get called in production mode when the assets are precompiled.
+#
+Rails.application.config.assets.paths.unshift "#{customization_directory}/stylesheets"
+
+#
+# Copy files to public
+#
+if !defined?(RAKE) && Dir.exists?("#{customization_directory}/public")
+ require 'fileutils'
+ FileUtils.cp_r("#{customization_directory}/public/.", "#{Rails.root}/public")
+end
+
+#
+# Add I18n path
+#
+Rails.application.config.i18n.load_path += Dir["#{customization_directory}/locales/*.{rb,yml,yaml}"]
diff --git a/core/Gemfile b/core/Gemfile
index 52ed377..b552dc5 100644
--- a/core/Gemfile
+++ b/core/Gemfile
@@ -1,4 +1,4 @@
-source "http://rubygems.org"
+source "https://rubygems.org"
# Declare your gem's dependencies in leap_web_core.gemspec.
# Bundler will treat runtime dependencies like base dependencies, and
diff --git a/core/app/assets/javascripts/platform.js b/core/app/assets/javascripts/platform.js
new file mode 100644
index 0000000..3ab77d7
--- /dev/null
+++ b/core/app/assets/javascripts/platform.js
@@ -0,0 +1,92 @@
+/* Inspired by mozillas platform detection:
+ https://github.com/mozilla/bedrock/tree/master/media/js/base
+*/
+ (function () {
+ 'use strict';
+ function getPlatform() {
+ var ua = navigator.userAgent,
+ pf = navigator.platform;
+ if (/Win(16|9[x58]|NT( [1234]| 5\.0| [^0-9]|[^ -]|$))/.test(ua) ||
+ /Windows ([MC]E|9[x58]|3\.1|4\.10|NT( [1234]| 5\.0| [^0-9]|[^ ]|$))/.test(ua) ||
+ /Windows_95/.test(ua)) {
+ /**
+ * Officially unsupported platforms are Windows 95, 98, ME, NT 4.x, 2000
+ * These regular expressions match:
+ * - Win16
+ * - Win9x
+ * - Win95
+ * - Win98
+ * - WinNT (not followed by version or followed by version <= 5)
+ * - Windows ME
+ * - Windows CE
+ * - Windows 9x
+ * - Windows 95
+ * - Windows 98
+ * - Windows 3.1
+ * - Windows 4.10
+ * - Windows NT (not followed by version or followed by version <= 5)
+ * - Windows_95
+ */
+ return 'oldwin';
+ }
+ if (ua.indexOf("MSIE 6.0") !== -1 &&
+ ua.indexOf("Windows NT 5.1") !== -1 &&
+ ua.indexOf("SV1") === -1) {
+ // Windows XP SP1
+ return 'oldwin';
+ }
+ if (pf.indexOf("Win32") !== -1 ||
+ pf.indexOf("Win64") !== -1) {
+ return 'windows';
+ }
+ if (/android/i.test(ua)) {
+ return 'android';
+ }
+ if (/armv[6-7]l/.test(pf)) {
+ return 'android';
+ }
+ if (pf.indexOf("Linux") !== -1) {
+ if (pf.indexOf("64") !== -1) {
+ return 'linux64';
+ } else {
+ return 'linux32';
+ }
+ }
+ if (pf.indexOf("MacPPC") !== -1) {
+ return 'oldmac';
+ }
+ if (/Mac OS X 10.[0-5]/.test(ua)) {
+ return 'oldmac';
+ }
+ if (pf.indexOf('iPhone') !== -1 ||
+ pf.indexOf('iPad') !== -1 ||
+ pf.indexOf('iPod') !== -1 ) {
+ return 'ios';
+ }
+ if (ua.indexOf("Mac OS X") !== -1) {
+ return 'osx';
+ }
+ if (ua.indexOf("MSIE 5.2") !== -1) {
+ return 'oldmac';
+ }
+ if (pf.indexOf("Mac") !== -1) {
+ return 'oldmac';
+ }
+ if (navigator.platform === '' &&
+ navigator.userAgent.indexOf("Firefox") !== -1 &&
+ navigator.userAgent.indexOf("Mobile") !== -1) {
+ return 'fxos';
+ }
+
+ return 'other';
+ }
+ (function () {
+ // Immediately set the platform classname on the html-element
+ // to avoid lots of flickering
+ var h = document.documentElement;
+ window.site = {
+ platform : getPlatform()
+ };
+ h.className = window.site.platform;
+ })();
+ })();
diff --git a/core/app/helpers/core_helper.rb b/core/app/helpers/core_helper.rb
index a496144..4126906 100644
--- a/core/app/helpers/core_helper.rb
+++ b/core/app/helpers/core_helper.rb
@@ -10,4 +10,4 @@ module CoreHelper
render 'common/home_page_buttons'
end
-end \ No newline at end of file
+end
diff --git a/core/app/helpers/download_helper.rb b/core/app/helpers/download_helper.rb
new file mode 100644
index 0000000..ee0fe73
--- /dev/null
+++ b/core/app/helpers/download_helper.rb
@@ -0,0 +1,33 @@
+module DownloadHelper
+
+ def alternative_client_links(os = nil)
+ alternative_clients(os).map do |client|
+ link_to(I18n.t("os."+client), client_download_url(client))
+ end
+ end
+
+ def alternative_clients(os = nil)
+ available_clients - [os]
+ end
+
+ def client_download_url(os = nil)
+ client_download_domain + client_download_path(os)
+ end
+
+ def client_download_path(os)
+ download_paths[os.to_s] || download_paths['other'] || ''
+ end
+
+ def available_clients
+ APP_CONFIG[:available_clients] || []
+ end
+
+ def client_download_domain
+ APP_CONFIG[:client_download_domain] || ''
+ end
+
+ def download_paths
+ APP_CONFIG[:download_paths] || {}
+ end
+
+end
diff --git a/core/app/views/common/_download_for_os.html.haml b/core/app/views/common/_download_for_os.html.haml
new file mode 100644
index 0000000..4c096ce
--- /dev/null
+++ b/core/app/views/common/_download_for_os.html.haml
@@ -0,0 +1,16 @@
+- os = download_for_os
+%div{:class => "os-#{os}"}
+ %span.link
+ - btn_class = (os == "other") ? "disabled" : "btn-primary"
+ = link_to client_download_url(os), :class => "btn btn-large #{btn_class}" do
+ .pull-left= huge_icon('mask')
+ = t(:download_client)
+ %br/
+ %small= I18n.t("os.#{os}")
+ %span.info
+ %div= t(:client_info, :provider => content_tag(:b,APP_CONFIG[:domain])).html_safe
+ %div
+ - if os == "other"
+ = t(:all_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe
+ - else
+ = t(:other_downloads_info, :clients => alternative_client_links(os).to_sentence).html_safe
diff --git a/core/app/views/common/_home_page_buttons.html.haml b/core/app/views/common/_home_page_buttons.html.haml
index 7eb4c40..3be12e2 100644
--- a/core/app/views/common/_home_page_buttons.html.haml
+++ b/core/app/views/common/_home_page_buttons.html.haml
@@ -2,11 +2,14 @@
.home-buttons
.row-fluid.first
- .span3
- .download.span6
- %span.link= link_to(big_icon('arrow-down', icon_color) + t(:download_client), "https://downloads.leap.se/client", :class => 'btn btn-large')
- %span.info= t(:download_client_info, :provider => content_tag(:b,APP_CONFIG[:domain])).html_safe
- .span3
+ .span2
+ .download.span8
+ = render partial: 'common/download_for_os', collection: available_clients + ['other']
+ .span2
+ - if local_assigns[:divider]
+ .row-fluid
+ .span12
+ = render local_assigns[:divider]
.row-fluid.second
.login.span4
%span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn')
diff --git a/core/config/locales/en.yml b/core/config/locales/en.yml
index 25b377a..4abf4e8 100644
--- a/core/config/locales/en.yml
+++ b/core/config/locales/en.yml
@@ -21,10 +21,20 @@ en:
are_you_sure: "Are you sure? This change cannot be undone."
download_client: "Download Bitmask"
- download_client_info: "The Bitmask application allows you to use %{provider} services. It is available for Linux, Mac, Windows, and Android."
+ client_info: "The Bitmask application allows you to use %{provider} services."
+ all_downloads_info: "It is available for %{clients}."
+ other_downloads_info: "Bitmask is also available for %{clients}."
login_info: "Log in to change your account settings, create support tickets, and manage payments."
signup_info: "Sign up for a new user account via this website (it is better if you use the Bitmask application to sign up, but this website works too)."
welcome: "Welcome to %{provider}."
get_help: "Get Help"
help_info: "Can't login? Create a new support ticket anonymously."
example_email: 'user@domain.org'
+ os:
+ linux32: "Linux (32 bit)"
+ linux64: "Linux (64 bit)"
+ windows: "Windows"
+ android: "Android"
+ osx: "Mac OS"
+ other: "(not available for your OS.)"
+
diff --git a/core/leap_web_core.gemspec b/core/leap_web_core.gemspec
index e98c892..7ca4d90 100644
--- a/core/leap_web_core.gemspec
+++ b/core/leap_web_core.gemspec
@@ -19,7 +19,7 @@ Gem::Specification.new do |s|
s.add_dependency "couchrest", "~> 1.1.3"
s.add_dependency "couchrest_model", "~> 2.0.0"
- s.add_dependency "couchrest_session_store", "~> 0.2.0"
+ s.add_dependency "couchrest_session_store", "~> 0.2.4"
s.add_dependency "json"
end
diff --git a/core/lib/extensions/couchrest.rb b/core/lib/extensions/couchrest.rb
index 91dfc1c..a9a195e 100644
--- a/core/lib/extensions/couchrest.rb
+++ b/core/lib/extensions/couchrest.rb
@@ -23,20 +23,18 @@ module CouchRest
end
end
- module Errors
- class ConnectionFailed < CouchRestModelError; end
- end
-
module Connection
module ClassMethods
def use_database(db)
@database = prepare_database(db)
- rescue RestClient::Unauthorized,
+ rescue RestClient::Exception,
Errno::EHOSTUNREACH,
Errno::ECONNREFUSED => e
- raise CouchRest::Model::Errors::ConnectionFailed.new(e.to_s)
+ message = "Could not connect to couch database #{db} due to #{e.to_s}"
+ Rails.logger.warn message
+ raise e.class.new(message) if APP_CONFIG[:reraise_errors]
end
end
@@ -47,28 +45,45 @@ module CouchRest
def self.load_all_models_with_engines
self.load_all_models_without_engines
return unless defined?(Rails)
- Dir[Rails.root + 'app/models/**/*.rb'].each do |path|
- require path
- end
Dir[Rails.root + '*/app/models/**/*.rb'].each do |path|
require path
end
end
- def self.all_models_and_proxies
- callbacks = migrate_each_model(find_models)
- callbacks += migrate_each_proxying_model(find_proxying_models)
- cleanup(callbacks)
+ class << self
+ alias_method_chain :load_all_models, :engines
+ end
+
+ def dump_all_models
+ prepare_directory
+ find_models.each do |model|
+ model.design_docs.each do |design|
+ dump_design(model, design)
+ end
+ end
end
+ protected
+ def dump_design(model, design)
+ dir = prepare_directory model.name.tableize
+ filename = design.id.sub('_design/','') + '.json'
+ puts dir + filename
+ design.checksum
+ File.open(dir + filename, "w") do |file|
+ file.write(JSON.pretty_generate(design.to_hash))
+ end
+ end
- class << self
- alias_method_chain :load_all_models, :engines
+ def prepare_directory(dir = '')
+ dir = Rails.root + 'tmp' + 'designs' + dir
+ Dir.mkdir(dir) unless Dir.exists?(dir)
+ return dir
end
end
end
+
end
class ModelRailtie
diff --git a/core/lib/tasks/leap_web_core_tasks.rake b/core/lib/tasks/leap_web_core_tasks.rake
index ae5b79b..ec6abac 100644
--- a/core/lib/tasks/leap_web_core_tasks.rake
+++ b/core/lib/tasks/leap_web_core_tasks.rake
@@ -1,4 +1,25 @@
-# desc "Explaining what the task does"
-# task :leap_web_core do
-# # Task goes here
-# end
+namespace :couchrest do
+
+ desc "Dump all the design docs found in each model"
+ task :dump => :environment do
+ CouchRest::Model::Utils::Migrate.load_all_models
+ CouchRest::Model::Utils::Migrate.dump_all_models
+ end
+end
+
+namespace :cleanup do
+
+ desc "Cleanup all expired session documents"
+ task :sessions => :environment do
+ # make sure this is the same as in
+ # config/initializers/session_store.rb
+ store = CouchRest::Session::Store.new expire_after: 1800
+ store.cleanup(store.expired)
+ end
+
+ desc "Cleanup all expired tokens"
+ task :tokens => :environment do
+ Token.destroy_all_expired
+ end
+end
+
diff --git a/help/Gemfile b/help/Gemfile
index 5e895e9..ad7d29b 100644
--- a/help/Gemfile
+++ b/help/Gemfile
@@ -1,4 +1,4 @@
-source "http://rubygems.org"
+source "https://rubygems.org"
eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb'))
eval(File.read(File.dirname(__FILE__) + '/..//ui_dependencies.rb'))
diff --git a/help/app/assets/javascripts/tickets.js b/help/app/assets/javascripts/tickets.js
index bf7965c..18537aa 100644
--- a/help/app/assets/javascripts/tickets.js
+++ b/help/app/assets/javascripts/tickets.js
@@ -1,4 +1,4 @@
//$(document).ready(function () {
// $.fn.editable.defaults.mode = 'inline';
-// $('#title').editable();
+// $('#subject').editable();
//}); \ No newline at end of file
diff --git a/help/app/controllers/tickets_controller.rb b/help/app/controllers/tickets_controller.rb
index a669e19..c193ff4 100644
--- a/help/app/controllers/tickets_controller.rb
+++ b/help/app/controllers/tickets_controller.rb
@@ -62,14 +62,11 @@ class TicketsController < ApplicationController
@ticket.comments.last.private = false unless admin?
end
- if @ticket.changed?
- if @ticket.save
- flash[:notice] = t(:changes_saved)
- redirect_to_tickets
- else
- respond_with @ticket
- end
+ if @ticket.changed? and @ticket.save
+ flash[:notice] = t(:changes_saved)
+ redirect_to_tickets
else
+ flash[:error] = @ticket.errors.full_messages.join(". ") if @ticket.changed?
redirect_to auto_ticket_path(@ticket)
end
end
diff --git a/help/app/models/account_extension/tickets.rb b/help/app/models/account_extension/tickets.rb
new file mode 100644
index 0000000..f898b56
--- /dev/null
+++ b/help/app/models/account_extension/tickets.rb
@@ -0,0 +1,13 @@
+module AccountExtension::Tickets
+ extend ActiveSupport::Concern
+
+ def destroy_with_tickets
+ Ticket.destroy_all_from(self.user)
+ destroy_without_tickets
+ end
+
+ included do
+ alias_method_chain :destroy, :tickets
+ end
+
+end
diff --git a/help/app/models/ticket.rb b/help/app/models/ticket.rb
index 8066d0d..cd22758 100644
--- a/help/app/models/ticket.rb
+++ b/help/app/models/ticket.rb
@@ -12,7 +12,7 @@ class Ticket < CouchRest::Model::Base
property :created_by, String, :protected => true # nil for anonymous tickets, should never be changed
property :regarding_user, String # may be nil or valid username
- property :title, String
+ property :subject, String
property :email, String
property :is_open, TrueClass, :default => true
property :comments, [TicketComment]
@@ -24,6 +24,7 @@ class Ticket < CouchRest::Model::Base
design do
view :by_updated_at
view :by_created_at
+ view :by_created_by
view :by_is_open_and_created_at
view :by_is_open_and_updated_at
@@ -32,7 +33,7 @@ class Ticket < CouchRest::Model::Base
load_views(own_path.join('..', 'designs', 'ticket'))
end
- validates :title, :presence => true
+ validates :subject, :presence => true
validates :email, :allow_blank => true, :format => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/
def self.search(options = {})
@@ -40,6 +41,12 @@ class Ticket < CouchRest::Model::Base
@selection.tickets
end
+ def self.destroy_all_from(user)
+ self.by_created_by.key(user.id).each do |ticket|
+ ticket.destroy
+ end
+ end
+
def is_creator_validated?
!!created_by
end
diff --git a/help/app/views/tickets/_edit_form.html.haml b/help/app/views/tickets/_edit_form.html.haml
index 5252c2e..714f8ff 100644
--- a/help/app/views/tickets/_edit_form.html.haml
+++ b/help/app/views/tickets/_edit_form.html.haml
@@ -1,7 +1,7 @@
:ruby
# created by user link
if @ticket.created_by_user
- created_by = link_to(@ticket.created_by_user.login, user_overview_path(@ticket.created_by_user))
+ created_by = link_to @ticket.created_by_user.login, @ticket.created_by_user
else
created_by = t(:anonymous)
end
@@ -9,7 +9,7 @@
# regarding user link
if admin?
if @ticket.regarding_user_actual_user
- regarding_user_link = link_to @ticket.regarding_user_actual_user.login, user_overview_path(@ticket.regarding_user_actual_user)
+ regarding_user_link = link_to @ticket.regarding_user_actual_user.login, @ticket.regarding_user_actual_user
else
regarding_user_link = "(#{t(:unknown)})"
end
@@ -26,7 +26,7 @@
%span.label.label-success= t(:closed)
%span.label.label-clear= t(:created_by_on, :user => created_by, :time => @ticket.created_at.to_s(:short)).html_safe
%div= t(:subject)
- = f.text_field :title, :class => 'large full-width'
+ = f.text_field :subject, :class => 'large full-width'
.row-fluid
.span4
%div= t(:status)
diff --git a/help/app/views/tickets/_ticket.html.haml b/help/app/views/tickets/_ticket.html.haml
index a064c4e..5bc33c8 100644
--- a/help/app/views/tickets/_ticket.html.haml
+++ b/help/app/views/tickets/_ticket.html.haml
@@ -1,6 +1,6 @@
- url = auto_ticket_path(ticket)
%tr
- %td= link_to ticket.title, url
+ %td= link_to ticket.subject, url
%td= link_to ticket.created_at.to_s(:short), url
%td= link_to ticket.updated_at.to_s(:short), url
%td= ticket.commenters
diff --git a/help/app/views/tickets/new.html.haml b/help/app/views/tickets/new.html.haml
index c0a343d..393e5d6 100644
--- a/help/app/views/tickets/new.html.haml
+++ b/help/app/views/tickets/new.html.haml
@@ -11,7 +11,7 @@
= simple_form_for @ticket, :validate => true, :html => {:class => 'form-horizontal'} do |f|
= hidden_ticket_fields
- = f.input :title, :label => t(:subject)
+ = f.input :subject
- if logged_in?
= f.input :email, input_html: {value: email}
= f.input :regarding_user, input_html: {value: regarding}
diff --git a/help/config/initializers/account_lifecycle.rb b/help/config/initializers/account_lifecycle.rb
new file mode 100644
index 0000000..d9f04c1
--- /dev/null
+++ b/help/config/initializers/account_lifecycle.rb
@@ -0,0 +1,3 @@
+ActiveSupport.on_load(:account) do
+ include AccountExtension::Tickets
+end
diff --git a/help/test/factories.rb b/help/test/factories.rb
index 5b38952..be04f15 100644
--- a/help/test/factories.rb
+++ b/help/test/factories.rb
@@ -1,9 +1,17 @@
FactoryGirl.define do
factory :ticket do
- title { Faker::Lorem.sentence }
- comments_attributes do
- { "0" => { "body" => Faker::Lorem.sentences.join(" ") } }
+ subject { Faker::Lorem.sentence }
+ email { Faker::Internet.email }
+
+ factory :ticket_with_comment do
+ comments_attributes do
+ { "0" => { "body" => Faker::Lorem.sentences.join(" ") } }
+ end
+ end
+
+ factory :ticket_with_creator do
+ created_by { FactoryGirl.create(:user).id }
end
end
diff --git a/help/test/functional/tickets_controller_test.rb b/help/test/functional/tickets_controller_test.rb
index 3747ad0..0f56e6e 100644
--- a/help/test/functional/tickets_controller_test.rb
+++ b/help/test/functional/tickets_controller_test.rb
@@ -53,7 +53,7 @@ class TicketsControllerTest < ActionController::TestCase
end
test "should create unauthenticated ticket" do
- params = {:title => "unauth ticket test title", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
+ params = {:subject => "unauth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
assert_difference('Ticket.count') do
post :create, :ticket => params
@@ -70,7 +70,7 @@ class TicketsControllerTest < ActionController::TestCase
test "should create authenticated ticket" do
- params = {:title => "auth ticket test title", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
+ params = {:subject => "auth ticket test subject", :comments_attributes => {"0" => {"body" =>"body of test ticket"}}}
login
diff --git a/help/test/unit/account_extension_test.rb b/help/test/unit/account_extension_test.rb
new file mode 100644
index 0000000..aba162c
--- /dev/null
+++ b/help/test/unit/account_extension_test.rb
@@ -0,0 +1,12 @@
+require 'test_helper'
+
+class AccountExtensionTest < ActiveSupport::TestCase
+
+ test "destroying an account triggers ticket destruction" do
+ t = FactoryGirl.create :ticket_with_creator
+ u = t.created_by_user
+ Account.new(u).destroy
+ assert_equal nil, Ticket.find(t.id)
+ end
+
+end
diff --git a/help/test/unit/ticket_comment_test.rb b/help/test/unit/ticket_comment_test.rb
index 44865ed..fe8cc95 100644
--- a/help/test/unit/ticket_comment_test.rb
+++ b/help/test/unit/ticket_comment_test.rb
@@ -36,7 +36,7 @@ class TicketCommentTest < ActiveSupport::TestCase
=end
test "add comments" do
- testticket = Ticket.create :title => "testing"
+ testticket = Ticket.create :subject => "testing"
assert_equal testticket.comments.count, 0
comment = TicketComment.new :body => "my email broke"
#assert comment.valid? #validating or saving necessary for setting posted_at
diff --git a/help/test/unit/ticket_test.rb b/help/test/unit/ticket_test.rb
index ce35e1d..f5e6ea7 100644
--- a/help/test/unit/ticket_test.rb
+++ b/help/test/unit/ticket_test.rb
@@ -1,57 +1,55 @@
require 'test_helper'
class TicketTest < ActiveSupport::TestCase
- #test "the truth" do
- # assert true
- #end
- setup do
- @sample = Ticket.new
+ test "ticket with default attribs is valid" do
+ t = FactoryGirl.build :ticket
+ assert t.valid?
end
- test "validity" do
- t = Ticket.create :title => 'test title', :email => 'blah@blah.com'
+ test "ticket without email is valid" do
+ t = FactoryGirl.build :ticket, email: ""
assert t.valid?
- assert_equal t.title, 'test title'
+ end
+ test "ticket validates email format" do
+ t = FactoryGirl.build :ticket, email: "aswerssfd"
+ assert !t.valid?
+ end
+
+ test "ticket open states" do
+ t = FactoryGirl.build :ticket
assert t.is_open
t.close
assert !t.is_open
t.reopen
assert t.is_open
- #user = LeapWebHelp::User.new(User.valid_attributes_hash)
- #user = LeapWebUsers::User.create
-
- #t.user = user
-
- #t.email = '' #invalid
- #assert !t.valid?
- #t.email = 'blah@blah.com, bb@jjj.org'
- #assert t.valid?
- t.email = 'bdlfjlkasfjklasjf' #invalid
- #p t.email_address
- #p t.email_address.strip =~ RFC822::EmailAddress
- assert !t.valid?
- t.reload.destroy
end
test "creation validated" do
+ @sample = Ticket.new
assert !@sample.is_creator_validated?
#p current_user
@sample.created_by = 22 #current_user
assert @sample.is_creator_validated?
end
+ test "destroy all tickets from a user" do
+ t = FactoryGirl.create :ticket_with_creator
+ u = t.created_by_user
+ Ticket.destroy_all_from(u)
+ assert_equal nil, Ticket.find(t.id)
+ 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'
+ t1 = Ticket.create :subject => 'test title'
assert_not_nil t1.code
assert_nil t1.created_by
User.current_test = 4
- t2 = Ticket.create :title => 'test title'
+ t2 = Ticket.create :subject => 'test title'
assert_nil t2.code
assert_not_nil t2.created_by
end
@@ -66,7 +64,7 @@ class TicketTest < ActiveSupport::TestCase
# TODO: the by_includes_post_by view is only used for tests. Maybe we should get rid of it and change the test to including ordering?
- testticket = Ticket.create :title => "test retrieving commented tickets"
+ testticket = Ticket.create :subject => "test retrieving commented tickets"
comment = TicketComment.new :body => "my email broke", :posted_by => "123"
assert_equal 0, testticket.comments.count
assert_equal [], Ticket.by_includes_post_by.key('123').all
diff --git a/lib/leap_web/version.rb b/lib/leap_web/version.rb
index a55c2ca..983e3ad 100644
--- a/lib/leap_web/version.rb
+++ b/lib/leap_web/version.rb
@@ -1,3 +1,3 @@
module LeapWeb
- VERSION = "0.2.4" unless defined?(LeapWeb::VERSION)
+ VERSION = "0.2.8" unless defined?(LeapWeb::VERSION)
end
diff --git a/public/leap-img/128/mask.png b/public/leap-img/128/mask.png
new file mode 100644
index 0000000..444a62c
--- /dev/null
+++ b/public/leap-img/128/mask.png
Binary files differ
diff --git a/test/integration/os_detection_test.rb b/test/integration/os_detection_test.rb
new file mode 100644
index 0000000..cb254aa
--- /dev/null
+++ b/test/integration/os_detection_test.rb
@@ -0,0 +1,24 @@
+require 'test_helper'
+
+class OsDetectionTest < BrowserIntegrationTest
+
+ setup do
+ Capybara.current_driver = Capybara.javascript_driver
+ end
+
+ test "old windows shows deactivated download" do
+ page.driver.headers = { "User-Agent" => "Win98" }
+ visit '/'
+ assert_selector "html.oldwin"
+ assert has_text? "not available"
+ end
+
+ test "android shows android download" do
+ page.driver.headers = { "User-Agent" => "Android" }
+ visit '/'
+ assert_selector "html.android"
+ assert has_no_text? "not available"
+ assert_selector "small", text: "Android"
+ end
+
+end
diff --git a/users/Gemfile b/users/Gemfile
index e30033a..4101ead 100644
--- a/users/Gemfile
+++ b/users/Gemfile
@@ -1,4 +1,4 @@
-source "http://rubygems.org"
+source "https://rubygems.org"
eval(File.read(File.dirname(__FILE__) + '/../common_dependencies.rb'))
eval(File.read(File.dirname(__FILE__) + '/../ui_dependencies.rb'))
diff --git a/users/app/assets/javascripts/srp b/users/app/assets/javascripts/srp
-Subproject d22bf3b9fe2fd31192e1e1b358e97e5a0f3f90b
+Subproject 8f33d32d40b1e21ae7fb9a92c78a275422af421
diff --git a/users/app/assets/javascripts/users.js b/users/app/assets/javascripts/users.js
index aaeba6e..8486756 100644
--- a/users/app/assets/javascripts/users.js
+++ b/users/app/assets/javascripts/users.js
@@ -46,6 +46,13 @@
$(form).find('input[type="submit"]').button('loading');
};
+ resetButtons = function(submitEvent) {
+ var form = $('form.submitted')
+ // bootstrap loading state:
+ $(form).find('input[type="submit"]').button('reset');
+ $(form).removeClass('submitted')
+ };
+
//
// PUBLIC FUNCTIONS
//
@@ -70,24 +77,36 @@
//
srp.error = function(message) {
clear_errors();
- var element, error, field;
+ var errors = extractErrors(message);
+ displayErrors(errors);
+ resetButtons();
+ }
+
+ function extractErrors(message) {
if ($.isPlainObject(message) && message.errors) {
- for (field in message.errors) {
- if (field == 'base') {
- alert_message(message.errors[field]);
- continue;
- }
- error = message.errors[field];
- element = $('form input[name$="[' + field + ']"]');
- if (!element) {
- continue;
- }
- element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false);
- }
- } else if (message.error) {
- alert_message(message.error);
+ return message.errors;
} else {
- alert_message(JSON.stringify(message));
+ return {
+ base: (message.error || JSON.stringify(message))
+ };
+ }
+ }
+
+ function displayErrors(errors) {
+ for (var field in errors) {
+ var error = errors[field];
+ if (field === 'base') {
+ alert_message(error);
+ } else {
+ displayFieldError(field, error);
+ }
+ }
+ }
+
+ function displayFieldError(field, error) {
+ var element = $('form input[name$="[' + field + ']"]');
+ if (element) {
+ element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false);
}
};
diff --git a/users/app/controllers/keys_controller.rb b/users/app/controllers/keys_controller.rb
new file mode 100644
index 0000000..fb28901
--- /dev/null
+++ b/users/app/controllers/keys_controller.rb
@@ -0,0 +1,18 @@
+class KeysController < ApplicationController
+
+ #
+ # Render the user's key as plain text, without a layout.
+ #
+ # We will show blank page if user doesn't have key (which shouldn't generally occur)
+ # and a 404 error if user doesn't exist
+ #
+ def show
+ user = User.find_by_login(params[:login])
+ if user
+ render text: user.public_key, content_type: 'text/text'
+ else
+ raise ActionController::RoutingError.new('Not Found')
+ end
+ end
+
+end
diff --git a/users/app/controllers/overviews_controller.rb b/users/app/controllers/overviews_controller.rb
deleted file mode 100644
index 52ce267..0000000
--- a/users/app/controllers/overviews_controller.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class OverviewsController < UsersBaseController
-
- before_filter :authorize
- before_filter :fetch_user
-
- def show
- end
-
-end
diff --git a/users/app/controllers/sessions_controller.rb b/users/app/controllers/sessions_controller.rb
index 0494b51..ca228c2 100644
--- a/users/app/controllers/sessions_controller.rb
+++ b/users/app/controllers/sessions_controller.rb
@@ -1,6 +1,7 @@
class SessionsController < ApplicationController
def new
+ redirect_to root_path if logged_in?
@session = Session.new
if authentication_errors
@errors = authentication_errors
@@ -14,12 +15,12 @@ class SessionsController < ApplicationController
end
#
- # this is a bad hack, but user_overview_url(user) is not available
+ # this is a bad hack, but user_url(user) is not available
# also, this doesn't work because the redirect happens as a PUT. no idea why.
#
#Warden::Manager.after_authentication do |user, auth, opts|
# response = Rack::Response.new
- # response.redirect "/users/#{user.id}/overview"
+ # response.redirect "/users/#{user.id}"
# throw :warden, response.finish
#end
diff --git a/users/app/controllers/users_controller.rb b/users/app/controllers/users_controller.rb
index f66277d..0b32ec7 100644
--- a/users/app/controllers/users_controller.rb
+++ b/users/app/controllers/users_controller.rb
@@ -13,7 +13,7 @@ class UsersController < UsersBaseController
def index
if params[:query]
if @user = User.find_by_login(params[:query])
- redirect_to user_overview_url(@user)
+ redirect_to @user
return
else
@users = User.by_login.startkey(params[:query]).endkey(params[:query].succ)
@@ -34,6 +34,12 @@ class UsersController < UsersBaseController
def edit
end
+ ## added so updating service level works, but not sure we will actually want this. also not sure that this is place to prevent user from updating own effective service level, but here as placeholder:
+ def update
+ @user.update_attributes(params[:user]) unless (!admin? and params[:user][:effective_service_level])
+ respond_with @user
+ end
+
def deactivate
@user.enabled = false
@user.save
@@ -47,8 +53,16 @@ class UsersController < UsersBaseController
end
def destroy
- @user.destroy
- redirect_to admin? ? users_url : root_url
+ @user.account.destroy
+ flash[:notice] = I18n.t(:account_destroyed)
+ # admins can destroy other users
+ if @user != current_user
+ redirect_to users_url
+ else
+ # let's remove the invalid session
+ logout
+ redirect_to root_url
+ end
end
end
diff --git a/users/app/controllers/v1/users_controller.rb b/users/app/controllers/v1/users_controller.rb
index 03a5a62..0903888 100644
--- a/users/app/controllers/v1/users_controller.rb
+++ b/users/app/controllers/v1/users_controller.rb
@@ -24,15 +24,9 @@ module V1
end
def update
- account.update params[:user]
+ @user.account.update params[:user]
respond_with @user
end
- protected
-
- def account
- @user.account
- end
-
end
end
diff --git a/users/app/models/account.rb b/users/app/models/account.rb
index 5368a1b..cf998e4 100644
--- a/users/app/models/account.rb
+++ b/users/app/models/account.rb
@@ -1,5 +1,10 @@
#
-# A Composition of a User record and it's identity records.
+# The Account model takes care of the livecycle of a user.
+# It composes a User record and it's identity records.
+# It also allows for other engines to hook into the livecycle by
+# monkeypatching the create, update and destroy methods.
+# There's an ActiveSupport load_hook at the end of this file to
+# make this more easy.
#
class Account
@@ -22,16 +27,15 @@ class Account
@user.update_attributes attrs.slice(:password_verifier, :password_salt)
end
# TODO: move into identity controller
- update_pgp_key(attrs[:public_key]) if attrs.has_key? :public_key
+ key = update_pgp_key(attrs[:public_key])
+ @user.errors.set :public_key, key.errors.full_messages
@user.save && save_identities
@user.refresh_identity
end
def destroy
return unless @user
- Identity.by_user_id.key(@user.id).each do |identity|
- identity.destroy
- end
+ Identity.disable_all_for(@user)
@user.destroy
end
@@ -46,12 +50,19 @@ class Account
end
def update_pgp_key(key)
- @new_identity ||= Identity.for(@user)
- @new_identity.set_key(:pgp, key)
+ PgpKey.new(key).tap do |key|
+ if key.present? && key.valid?
+ @new_identity ||= Identity.for(@user)
+ @new_identity.set_key(:pgp, key)
+ end
+ end
end
def save_identities
@new_identity.try(:save) && @old_identity.try(:save)
end
+ # You can hook into the account lifecycle from different engines using
+ # ActiveSupport.on_load(:account) do ...
+ ActiveSupport.run_load_hooks(:account, self)
end
diff --git a/users/app/models/identity.rb b/users/app/models/identity.rb
index e0a24e9..cbb540e 100644
--- a/users/app/models/identity.rb
+++ b/users/app/models/identity.rb
@@ -27,6 +27,17 @@ class Identity < CouchRest::Model::Base
emit(doc.address, doc.keys["pgp"]);
}
EOJS
+ view :disabled,
+ map: <<-EOJS
+ function(doc) {
+ if (doc.type != 'Identity') {
+ return;
+ }
+ if (typeof doc.user_id === "undefined") {
+ emit(doc._id, 1);
+ }
+ }
+ EOJS
end
@@ -50,6 +61,19 @@ class Identity < CouchRest::Model::Base
identity
end
+ def self.disable_all_for(user)
+ Identity.by_user_id.key(user.id).each do |identity|
+ identity.disable
+ identity.save
+ end
+ end
+
+ def self.destroy_all_disabled
+ Identity.disabled.each do |identity|
+ identity.destroy
+ end
+ end
+
def self.attributes_from_user(user)
{ user_id: user.id,
address: user.email_address,
@@ -57,13 +81,22 @@ class Identity < CouchRest::Model::Base
}
end
+ def enabled?
+ self.destination && self.user_id
+ end
+
+ def disable
+ self.destination = nil
+ self.user_id = nil
+ end
+
def keys
read_attribute('keys') || HashWithIndifferentAccess.new
end
- def set_key(type, value)
- return if keys[type] == value
- write_attribute('keys', keys.merge(type => value))
+ def set_key(type, key)
+ return if keys[type] == key.to_s
+ write_attribute('keys', keys.merge(type => key.to_s))
end
# for LoginFormatValidation
@@ -93,7 +126,8 @@ class Identity < CouchRest::Model::Base
end
def destination_email
- return if destination.valid? #this ensures it is Email
+ return if destination.nil? # this identity is disabled
+ return if destination.valid? # this ensures it is Email
self.errors.add(:destination, destination.errors.messages[:email].first) #assumes only one error #TODO
end
diff --git a/users/app/models/local_email.rb b/users/app/models/local_email.rb
index 6303bb6..2b4c65e 100644
--- a/users/app/models/local_email.rb
+++ b/users/app/models/local_email.rb
@@ -1,5 +1,10 @@
class LocalEmail < Email
+ BLACKLIST_FROM_RFC2142 = [
+ 'postmaster', 'hostmaster', 'domainadmin', 'webmaster', 'www',
+ 'abuse', 'noc', 'security', 'usenet', 'news', 'uucp',
+ 'ftp', 'sales', 'marketing', 'support', 'info'
+ ]
def self.domain
APP_CONFIG[:domain]
@@ -11,6 +16,8 @@ class LocalEmail < Email
:message => "needs to end in @#{domain}"
}
+ validate :handle_allowed
+
def initialize(s)
super
append_domain_if_needed
@@ -32,4 +39,30 @@ class LocalEmail < Email
end
end
+ def handle_allowed
+ errors.add(:handle, "is reserved.") if handle_reserved?
+ end
+
+ def handle_reserved?
+ # *ARRAY in a case statement tests if ARRAY includes the handle.
+ case handle
+ when *APP_CONFIG[:handle_blacklist]
+ true
+ when *APP_CONFIG[:handle_whitelist]
+ false
+ when *BLACKLIST_FROM_RFC2142
+ true
+ else
+ handle_in_passwd?
+ end
+ end
+
+ def handle_in_passwd?
+ begin
+ !!Etc.getpwnam(handle)
+ rescue ArgumentError
+ # handle was not found
+ return false
+ end
+ end
end
diff --git a/users/app/models/pgp_key.rb b/users/app/models/pgp_key.rb
new file mode 100644
index 0000000..66f8660
--- /dev/null
+++ b/users/app/models/pgp_key.rb
@@ -0,0 +1,48 @@
+class PgpKey
+ include ActiveModel::Validations
+
+ KEYBLOCK_IDENTIFIERS = [
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----',
+ '-----END PGP PUBLIC KEY BLOCK-----',
+ ]
+
+ # mostly for testing.
+ attr_accessor :keyblock
+
+ validate :validate_keyblock_format
+
+ def initialize(keyblock = nil)
+ @keyblock = keyblock
+ end
+
+ def to_s
+ @keyblock
+ end
+
+ def present?
+ @keyblock.present?
+ end
+
+ # allow comparison with plain keyblock strings.
+ def ==(other)
+ self.equal?(other) or
+ # relax the comparison on line ends.
+ self.to_s.tr_s("\n\r", '') == other.tr_s("\r\n", '')
+ end
+
+ protected
+
+ def validate_keyblock_format
+ if keyblock_identifier_missing?
+ errors.add :public_key_block,
+ "does not look like an armored pgp public key block"
+ end
+ end
+
+ def keyblock_identifier_missing?
+ KEYBLOCK_IDENTIFIERS.find do |identify|
+ !@keyblock.include?(identify)
+ end
+ end
+
+end
diff --git a/users/app/models/service_level.rb b/users/app/models/service_level.rb
new file mode 100644
index 0000000..299aaf1
--- /dev/null
+++ b/users/app/models/service_level.rb
@@ -0,0 +1,19 @@
+class ServiceLevel
+
+ def initialize(attributes = {})
+ @id = attributes[:id] || APP_CONFIG[:default_service_level]
+ end
+
+ def self.authenticated_select_options
+ APP_CONFIG[:service_levels].map { |id,config_hash| [config_hash[:description], id] if config_hash[:name] != 'anonymous'}.compact
+ end
+
+ def id
+ @id
+ end
+
+ def config_hash
+ APP_CONFIG[:service_levels][@id]
+ end
+
+end
diff --git a/users/app/models/token.rb b/users/app/models/token.rb
index dd87344..001eb40 100644
--- a/users/app/models/token.rb
+++ b/users/app/models/token.rb
@@ -11,6 +11,25 @@ class Token < CouchRest::Model::Base
validates :user_id, presence: true
+ design do
+ view :by_last_seen_at
+ end
+
+ def self.expires_after
+ APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after]
+ end
+
+ def self.expired
+ return [] unless expires_after
+ by_last_seen_at.endkey(expires_after.minutes.ago)
+ end
+
+ def self.destroy_all_expired
+ self.expired.each do |token|
+ token.destroy
+ end
+ end
+
def authenticate
if expired?
destroy
@@ -27,21 +46,16 @@ class Token < CouchRest::Model::Base
end
def expired?
- expires_after and
- last_seen_at + expires_after.minutes < Time.now
- end
-
- def expires_after
- APP_CONFIG[:auth] && APP_CONFIG[:auth][:token_expires_after]
+ Token.expires_after and
+ last_seen_at < Token.expires_after.minutes.ago
end
def initialize(*args)
super
- self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '')
- self.last_seen_at = Time.now
- end
-
- design do
+ if new_record?
+ self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '')
+ self.last_seen_at = Time.now
+ end
end
end
diff --git a/users/app/models/unauthenticated_user.rb b/users/app/models/unauthenticated_user.rb
index 99a6874..0fc17d2 100644
--- a/users/app/models/unauthenticated_user.rb
+++ b/users/app/models/unauthenticated_user.rb
@@ -1,4 +1,6 @@
# The nil object for the user class
class UnauthenticatedUser < Object
+ # will probably want something here to return service level as APP_CONFIG[:service_levels][0] but not sure how will be accessing.
+
end
diff --git a/users/app/models/user.rb b/users/app/models/user.rb
index a14fcb5..720f5a9 100644
--- a/users/app/models/user.rb
+++ b/users/app/models/user.rb
@@ -9,6 +9,12 @@ class User < CouchRest::Model::Base
property :enabled, TrueClass, :default => true
+ # these will be null by default but we shouldn't ever pull them directly, but only via the methods that will return the full ServiceLevel
+ property :desired_service_level_code, Integer, :accessible => true
+ property :effective_service_level_code, Integer, :accessible => true
+
+ before_save :update_effective_service_level
+
validates :login, :password_salt, :password_verifier,
:presence => true
@@ -94,6 +100,16 @@ class User < CouchRest::Model::Base
@identity = Identity.for(self)
end
+ def desired_service_level
+ code = self.desired_service_level_code || APP_CONFIG[:default_service_level]
+ ServiceLevel.new({id: code})
+ end
+
+ def effective_service_level
+ code = self.effective_service_level_code || self.desired_service_level.id
+ ServiceLevel.new({id: code})
+ end
+
protected
##
@@ -116,4 +132,12 @@ class User < CouchRest::Model::Base
def serverside?
true
end
+
+ def update_effective_service_level
+ # TODO: Is this always the case? Might there be a situation where the admin has set the effective service level and we don't want it changed to match the desired one?
+ if self.desired_service_level_code_changed?
+ self.effective_service_level_code = self.desired_service_level_code
+ end
+ end
+
end
diff --git a/users/app/views/overviews/show.html.haml b/users/app/views/overviews/show.html.haml
deleted file mode 100644
index d3409df..0000000
--- a/users/app/views/overviews/show.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-.overview
-
- %h2.first= t(:overview_welcome, :username => @user.login)
-
- - if admin?
- %p
- = t(:created)
- = @user.created_at
- %br
- = t(:updated)
- = @user.updated_at
- %br
- = t(:enabled)
- = @user.enabled?
-
- %p= t(:overview_intro)
-
- %ul.unstyled
- %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user))
- - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly}
- %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user))
- %li= icon('shopping-cart') + link_to(t(:overview_billing), show_or_new_customer_link(@user)) if APP_CONFIG[:payment].present?
diff --git a/users/app/views/users/_change_password.html.haml b/users/app/views/users/_change_password.html.haml
new file mode 100644
index 0000000..425e3ee
--- /dev/null
+++ b/users/app/views/users/_change_password.html.haml
@@ -0,0 +1,21 @@
+-#
+-# CHANGE PASSWORD
+-#
+-# * everything about this form is handled with javascript. So take care when changing any ids.
+-# * the login is required when changing the password because it is used as part of the salt when calculating the password verifier.
+-# however, we don't want the user to change their login without generating a new key, so we hide the ui for this
+-# (although it works perfectly fine to change username if the field was visible).
+-#
+
+- form_options = {:url => '/not-used', :html => {:class => user_form_class('form-horizontal'), :id => 'update_login_and_password', :data => {token: session[:token]}}, :validate => true}
+= simple_form_for @user, form_options do |f|
+ %legend= t(:change_password)
+ = hidden_field_tag 'user_param', @user.to_param
+ .hidden
+ = f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username}
+ = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password }
+ = f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation }
+ .control-group
+ .controls
+ = f.submit t(:save), :class => 'btn btn-primary'
+
diff --git a/users/app/views/users/_change_pgp_key.html.haml b/users/app/views/users/_change_pgp_key.html.haml
new file mode 100644
index 0000000..e465125
--- /dev/null
+++ b/users/app/views/users/_change_pgp_key.html.haml
@@ -0,0 +1,13 @@
+-#
+-# CHANGE PGP KEY
+-#
+-# this will be replaced by a identities controller/view at some point
+-#
+
+- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_pgp_key', :data => {token: session[:token]}}, :validate => true}
+= simple_form_for [:api, @user], form_options do |f|
+ %legend= t(:advanced_options)
+ = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
+ .control-group
+ .controls
+ = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/users/app/views/users/_change_service_level.html.haml b/users/app/views/users/_change_service_level.html.haml
new file mode 100644
index 0000000..61e67d9
--- /dev/null
+++ b/users/app/views/users/_change_service_level.html.haml
@@ -0,0 +1,18 @@
+-# TODO: probably won't want here, but here for now. Also, we will need way to ensure payment if they pick a non-free plan.
+-#
+-# SERVICE LEVEL
+-#
+- if APP_CONFIG[:service_levels]
+ - form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_service_level', :data => {token: session[:token]}}, :validate => true}
+ = simple_form_for @user, form_options do |f|
+ %legend= t(:service_level)
+ - if @user != current_user
+ = t(:desired_service_level)
+ = f.select :desired_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.desired_service_level.id
+ - if @user != current_user
+ %p
+ = t(:effective_service_level)
+ = f.select :effective_service_level_code, ServiceLevel.authenticated_select_options, :selected => @user.effective_service_level.id
+ .control-group
+ .controls
+ = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
diff --git a/users/app/views/users/_destroy_account.html.haml b/users/app/views/users/_destroy_account.html.haml
new file mode 100644
index 0000000..445f3c4
--- /dev/null
+++ b/users/app/views/users/_destroy_account.html.haml
@@ -0,0 +1,27 @@
+-#
+-# DESTROY ACCOUNT
+-#
+
+%legend
+ - if @user == current_user
+ = t(:destroy_my_account)
+ - else
+ = t(:admin_destroy_account, :username => @user.login)
+%p= t(:destroy_account_info)
+= link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do
+ %i.icon-remove.icon-white
+ = t(:destroy_my_account)
+- if @user != current_user and @user.enabled?
+ %legend
+ = t(:deactivate_account, :username => @user.login)
+ %p= t(:deactivate_description)
+ = link_to deactivate_user_path(@user), :method => :post, :class => "btn btn-warning" do
+ %i.icon-pause.icon-white
+ = t(:deactivate)
+- elsif @user != current_user and !@user.enabled?
+ %legend
+ = t(:enable_account, :username => @user.login)
+ %p= t(:enable_description)
+ = link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do
+ %i.icon-ok.icon-white
+ = t(:enable)
diff --git a/users/app/views/users/_edit.html.haml b/users/app/views/users/_edit.html.haml
index 9d2473b..1d2b68a 100644
--- a/users/app/views/users/_edit.html.haml
+++ b/users/app/views/users/_edit.html.haml
@@ -1,66 +1,14 @@
-#
-# edit user form, used by both show and edit actions.
-#
-
--#
--# CHANGE PASSWORD
--#
--# * everything about this form is handled with javascript. So take care when changing any ids.
--# * the login is required when changing the password because it is used as part of the salt when calculating the password verifier.
--# however, we don't want the user to change their login without generating a new key, so we hide the ui for this
--# (although it works perfectly fine to change username if the field was visible).
--#
-
-- form_options = {:url => '/not-used', :html => {:class => user_form_class('form-horizontal'), :id => 'update_login_and_password', :data => {token: session[:token]}}, :validate => true}
-= simple_form_for @user, form_options do |f|
- %legend= t(:change_password)
- = hidden_field_tag 'user_param', @user.to_param
- .hidden
- = f.input :login, :label => t(:username), :required => false, :input_html => {:id => :srp_username}
- = f.input :password, :required => false, :validate => true, :input_html => { :id => :srp_password }
- = f.input :password_confirmation, :required => false, :input_html => { :id => :srp_password_confirmation }
- .control-group
- .controls
- = f.submit t(:save), :class => 'btn btn-primary'
-
--#
--# CHANGE PGP KEY
--#
--# this will be replaced by a identities controller/view at some point
--#
-
-- form_options = {:html => {:class => user_form_class('form-horizontal'), :id => 'update_pgp_key', :data => {token: session[:token]}}, :validate => true}
-= simple_form_for [:api, @user], form_options do |f|
- %legend= t(:advanced_options)
- = f.input :public_key, :as => :text, :hint => t(:use_ascii_key), :input_html => {:class => "full-width", :rows => 4}
- .control-group
- .controls
- = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."}
-
--#
--# DESTROY ACCOUNT
--#
-
-%legend
- - if @user == current_user
- = t(:destroy_my_account)
- - else
- = t(:admin_destroy_account, :username => @user.login)
-%p= t(:destroy_account_info)
-= link_to user_path(@user), :method => :delete, :confirm => t(:are_you_sure), :class => "btn btn-danger" do
- %i.icon-remove.icon-white
- = t(:destroy_my_account)
-- if @user != current_user and @user.enabled?
- %legend
- = t(:deactivate_account, :username => @user.login)
- %p= t(:deactivate_description)
- = link_to deactivate_user_path(@user), :method => :post, :class => "btn btn-warning" do
- %i.icon-pause.icon-white
- = t(:deactivate)
-- elsif @user != current_user and !@user.enabled?
- %legend
- = t(:enable_account, :username => @user.login)
- %p= t(:enable_description)
- = link_to enable_user_path(@user), :method => :post, :class => "btn btn-warning" do
- %i.icon-ok.icon-white
- = t(:enable)
+-# We render a bunch of forms here. Which we use depends upon config settings
+-# user_actions and admin_actions. They both include an array of actions
+-# allowed to users and admins.
+-# Possible forms are:
+-# 'change_password'
+-# 'change_pgp_key'
+-# 'change_service_level'
+-# 'destroy_account'
+- actions = APP_CONFIG[admin? ? :admin_actions : :user_actions] || []
+- actions.each do |action|
+ = render action
diff --git a/users/app/views/users/_user.html.haml b/users/app/views/users/_user.html.haml
index 990d9cf..583d22f 100644
--- a/users/app/views/users/_user.html.haml
+++ b/users/app/views/users/_user.html.haml
@@ -1,4 +1,4 @@
%tr
- %td= link_to user.login, user_overview_path(user)
+ %td= link_to user.login, user
%td= l(user.created_at, :format => :short)
%td= l(user.updated_at, :format => :short)
diff --git a/users/app/views/users/show.html.haml b/users/app/views/users/show.html.haml
index 434c025..7bea370 100644
--- a/users/app/views/users/show.html.haml
+++ b/users/app/views/users/show.html.haml
@@ -1 +1,22 @@
-= render 'edit'
+.overview
+
+ %h2.first= t(:overview_welcome, :username => @user.login)
+
+ - if admin?
+ %p
+ = t(:created)
+ = @user.created_at
+ %br
+ = t(:updated)
+ = @user.updated_at
+ %br
+ = t(:enabled)
+ = @user.enabled?
+
+ %p= t(:overview_intro)
+
+ %ul.unstyled
+ %li= icon('user') + link_to(t(:overview_account), edit_user_path(@user))
+ - # %li= icon('envelope') + link_to(t(:overview_email), {insert path for user identities, presuambly}
+ %li= icon('question-sign') + link_to(t(:overview_tickets), user_tickets_path(@user))
+ %li= icon('shopping-cart') + link_to(t(:overview_billing), billing_top_link(@user)) if APP_CONFIG[:payment].present?
diff --git a/users/config/locales/en.yml b/users/config/locales/en.yml
index b69f7f4..1b5dd5e 100644
--- a/users/config/locales/en.yml
+++ b/users/config/locales/en.yml
@@ -17,6 +17,7 @@ en:
destroy_my_account: "Destroy my account"
destroy_account_info: "This will permanently destroy your account and all the data associated with it. Proceed with caution!"
admin_destroy_account: "Destroy the account %{username}"
+ account_destroyed: "The account has been destroyed successfully."
set_email_address: "Set email address"
forward_email: "Forward Email"
email_aliases: "Email Aliases"
diff --git a/users/config/routes.rb b/users/config/routes.rb
index ccecfd5..de2ff37 100644
--- a/users/config/routes.rb
+++ b/users/config/routes.rb
@@ -13,13 +13,14 @@ Rails.application.routes.draw do
get "signup" => "users#new", :as => "signup"
resources :users, :except => [:create, :update] do
- resource :overview, :only => [:show]
# resource :email_settings, :only => [:edit, :update]
- resources :email_aliases, :only => [:destroy], :id => /.*/
+ # resources :email_aliases, :only => [:destroy], :id => /.*/
post 'deactivate', on: :member
post 'enable', on: :member
end
get "/.well-known/host-meta" => 'webfinger#host_meta'
get "/webfinger" => 'webfinger#search'
+ get "/key/:login" => 'keys#show'
+
end
diff --git a/users/test/factories.rb b/users/test/factories.rb
index c87e290..ae00d43 100644
--- a/users/test/factories.rb
+++ b/users/test/factories.rb
@@ -19,6 +19,16 @@ FactoryGirl.define do
end
end
- factory :token
+ factory :token do
+ user
+ end
+ factory :pgp_key do
+ keyblock <<-EOPGP
+-----BEGIN PGP PUBLIC KEY BLOCK-----
++Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+++Dummy+PGP+KEY+
+#{SecureRandom.base64(4032)}
+-----END PGP PUBLIC KEY BLOCK-----
+ EOPGP
+ end
end
diff --git a/users/test/functional/keys_controller_test.rb b/users/test/functional/keys_controller_test.rb
new file mode 100644
index 0000000..863be93
--- /dev/null
+++ b/users/test/functional/keys_controller_test.rb
@@ -0,0 +1,32 @@
+require 'test_helper'
+
+class KeysControllerTest < ActionController::TestCase
+
+ test "get existing public key" do
+ public_key = 'my public key'
+ @user = stub_record :user, :public_key => public_key
+ User.stubs(:find_by_login).with(@user.login).returns(@user)
+ get :show, :login => @user.login
+ assert_response :success
+ assert_equal "text/text", response.content_type
+ assert_equal public_key, response.body
+ end
+
+ test "get non-existing public key for user" do
+ # this isn't a scenerio that should generally occur.
+ @user = stub_record :user
+ User.stubs(:find_by_login).with(@user.login).returns(@user)
+ get :show, :login => @user.login
+ assert_response :success
+ assert_equal "text/text", response.content_type
+ assert_equal '', response.body.strip
+ end
+
+ test "get public key for non-existing user" do
+ # raise 404 error if user doesn't exist (doesn't need to be this routing error, but seems fine to assume for now):
+ assert_raise(ActionController::RoutingError) {
+ get :show, :login => 'asdkljslksjfdlskfj'
+ }
+ end
+
+end
diff --git a/users/test/functional/sessions_controller_test.rb b/users/test/functional/sessions_controller_test.rb
index a630e6e..8b49005 100644
--- a/users/test/functional/sessions_controller_test.rb
+++ b/users/test/functional/sessions_controller_test.rb
@@ -17,6 +17,13 @@ class SessionsControllerTest < ActionController::TestCase
assert_template "sessions/new"
end
+ test "redirect to root_url if logged in" do
+ login
+ get :new
+ assert_response :redirect
+ assert_redirected_to root_url
+ end
+
test "renders json" do
get :new, :format => :json
assert_response :success
@@ -41,20 +48,12 @@ class SessionsControllerTest < ActionController::TestCase
assert_json_error :login => I18n.t(:all_strategies_failed)
end
- test "logout should reset warden user" do
- expect_warden_logout
+ test "destory should logout" do
+ login
+ expect_logout
delete :destroy
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 052de04..9c5f8d9 100644
--- a/users/test/functional/users_controller_test.rb
+++ b/users/test/functional/users_controller_test.rb
@@ -77,7 +77,11 @@ class UsersControllerTest < ActionController::TestCase
test "admin can destroy user" do
user = find_record :user
+
+ # we destroy the user record and the associated data...
user.expects(:destroy)
+ Identity.expects(:disable_all_for).with(user)
+ Ticket.expects(:destroy_all_from).with(user)
login :is_admin? => true
delete :destroy, :id => user.id
@@ -88,9 +92,14 @@ class UsersControllerTest < ActionController::TestCase
test "user can cancel account" do
user = find_record :user
+
+ # we destroy the user record and the associated data...
user.expects(:destroy)
+ Identity.expects(:disable_all_for).with(user)
+ Ticket.expects(:destroy_all_from).with(user)
login user
+ expect_logout
delete :destroy, :id => @current_user.id
assert_response :redirect
diff --git a/users/test/functional/v1/sessions_controller_test.rb b/users/test/functional/v1/sessions_controller_test.rb
index ff9fca1..4200e8f 100644
--- a/users/test/functional/v1/sessions_controller_test.rb
+++ b/users/test/functional/v1/sessions_controller_test.rb
@@ -52,26 +52,11 @@ class V1::SessionsControllerTest < ActionController::TestCase
assert_equal @user.id, token.user_id
end
- test "logout should reset session" do
- expect_warden_logout
- delete :destroy
- assert_response 204
- end
-
- test "logout should destroy token" do
+ test "destroy should logout" do
login
- expect_warden_logout
- @token.expects(:destroy)
+ expect_logout
delete :destroy
assert_response 204
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/integration/api/account_flow_test.rb b/users/test/integration/api/account_flow_test.rb
index e41befa..edd0859 100644
--- a/users/test/integration/api/account_flow_test.rb
+++ b/users/test/integration/api/account_flow_test.rb
@@ -96,27 +96,41 @@ class AccountFlowTest < RackTest
assert server_auth["M2"]
end
- test "update user" do
+ test "prevent changing login without changing password_verifier" do
server_auth = @srp.authenticate(self)
- test_public_key = 'asdlfkjslfdkjasd'
original_login = @user.login
new_login = 'zaph'
User.find_by_login(new_login).try(:destroy)
Identity.by_address.key(new_login + '@' + APP_CONFIG[:domain]).each do |identity|
identity.destroy
end
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => test_public_key, :login => new_login}, :format => :json
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:login => new_login}, :format => :json
assert last_response.successful?
- assert_equal test_public_key, Identity.for(@user).keys[:pgp]
# does not change login if no password_verifier is present
assert_equal original_login, @user.login
- # eventually probably want to remove most of this into a non-integration functional test
- # should not overwrite public key:
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:blee => :blah}, :format => :json
- assert_equal test_public_key, Identity.for(@user).keys[:pgp]
- # should overwrite public key:
- put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => nil}, :format => :json
+ end
+
+ test "upload pgp key" do
+ server_auth = @srp.authenticate(self)
+ key = FactoryGirl.build :pgp_key
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => key}, :format => :json
+ assert_equal key, Identity.for(@user).keys[:pgp]
+ end
+
+ # eventually probably want to remove most of this into a non-integration
+ # functional test
+ test "prevent uploading invalid key" do
+ server_auth = @srp.authenticate(self)
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => :blah}, :format => :json
assert_nil Identity.for(@user).keys[:pgp]
end
+ test "prevent emptying public key" do
+ server_auth = @srp.authenticate(self)
+ key = FactoryGirl.build :pgp_key
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => key}, :format => :json
+ put "http://api.lvh.me:3000/1/users/" + @user.id + '.json', :user => {:public_key => ""}, :format => :json
+ assert_equal key, Identity.for(@user).keys[:pgp]
+ end
+
end
diff --git a/users/test/integration/api/python/umlauts.py b/users/test/integration/api/python/umlauts.py
new file mode 100755
index 0000000..96fecbf
--- /dev/null
+++ b/users/test/integration/api/python/umlauts.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# under development
+
+import requests
+import json
+import string
+import random
+import srp._pysrp as srp
+import binascii
+
+safe_unhexlify = lambda x: binascii.unhexlify(x) if (len(x) % 2 == 0) else binascii.unhexlify('0'+x)
+
+# using globals for now
+# server = 'https://dev.bitmask.net/1'
+server = 'http://api.lvh.me:3000/1'
+
+def run_tests():
+ login = 'test_' + id_generator()
+ password = id_generator() + "äöì" + id_generator()
+ usr = srp.User( login, password, srp.SHA256, srp.NG_1024 )
+ print_and_parse(signup(login, password))
+
+ auth = print_and_parse(authenticate(usr))
+ verify_or_debug(auth, usr)
+ assert usr.authenticated()
+
+
+# let's have some random name
+def id_generator(size=6, chars=string.ascii_lowercase + string.digits):
+ return ''.join(random.choice(chars) for x in range(size))
+
+# log the server communication
+def print_and_parse(response):
+ request = response.request
+ print request.method + ': ' + response.url
+ if hasattr(request, 'data'):
+ print " " + json.dumps(response.request.data)
+ print " -> " + response.text
+ try:
+ return json.loads(response.text)
+ except ValueError:
+ return None
+
+def signup(login, password):
+ salt, vkey = srp.create_salted_verification_key( login, password, srp.SHA256, srp.NG_1024 )
+ user_params = {
+ 'user[login]': login,
+ 'user[password_verifier]': binascii.hexlify(vkey),
+ 'user[password_salt]': binascii.hexlify(salt)
+ }
+ print json.dumps(user_params)
+ return requests.post(server + '/users.json', data = user_params, verify = False)
+
+def authenticate(usr):
+ session = requests.session()
+ uname, A = usr.start_authentication()
+ params = {
+ 'login': uname,
+ 'A': binascii.hexlify(A)
+ }
+ init = print_and_parse(session.post(server + '/sessions', data = params, verify=False))
+ M = usr.process_challenge( safe_unhexlify(init['salt']), safe_unhexlify(init['B']) )
+ return session.put(server + '/sessions/' + uname, verify = False,
+ data = {'client_auth': binascii.hexlify(M)})
+
+def verify_or_debug(auth, usr):
+ if ( 'errors' in auth ):
+ print ' u = "%x"' % usr.u
+ print ' x = "%x"' % usr.x
+ print ' v = "%x"' % usr.v
+ print ' S = "%x"' % usr.S
+ print ' K = "' + binascii.hexlify(usr.K) + '"'
+ print ' M = "' + binascii.hexlify(usr.M) + '"'
+ else:
+ usr.verify_session( safe_unhexlify(auth["M2"]) )
+
+run_tests()
diff --git a/users/test/integration/browser/account_test.rb b/users/test/integration/browser/account_test.rb
index 1deda45..4cefe35 100644
--- a/users/test/integration/browser/account_test.rb
+++ b/users/test/integration/browser/account_test.rb
@@ -6,6 +6,10 @@ class AccountTest < BrowserIntegrationTest
Capybara.current_driver = Capybara.javascript_driver
end
+ teardown do
+ Identity.destroy_all_disabled
+ end
+
test "normal account workflow" do
username, password = submit_signup
assert page.has_content?("Welcome #{username}")
@@ -19,46 +23,85 @@ class AccountTest < BrowserIntegrationTest
test "successful login" do
username, password = submit_signup
click_on 'Logout'
- click_on 'Log In'
- fill_in 'Username', with: username
- fill_in 'Password', with: password
- click_on 'Log In'
+ attempt_login(username, password)
assert page.has_content?("Welcome #{username}")
User.find_by_login(username).account.destroy
end
- test "change password" do
+ test "failed login" do
+ visit '/'
+ attempt_login("username", "wrong password")
+ assert_invalid_login(page)
+ end
+
+ test "account destruction" do
+ username, password = submit_signup
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ assert page.has_content?(I18n.t('account_destroyed'))
+ attempt_login(username, password)
+ assert_invalid_login(page)
+ end
+
+ test "handle blocked after account destruction" do
+ username, password = submit_signup
+ click_on I18n.t('account_settings')
+ click_on I18n.t('destroy_my_account')
+ submit_signup(username)
+ assert page.has_content?('has already been taken')
+ end
+
+ test "default user actions" do
username, password = submit_signup
click_on "Account Settings"
- within('#update_login_and_password') do
- fill_in 'Password', with: "other password"
- fill_in 'Password confirmation', with: "other password"
- click_on 'Save'
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_no_css? '#update_pgp_key'
+ end
+
+ test "default admin actions" do
+ username, password = submit_signup
+ with_config admins: [username] do
+ click_on "Account Settings"
+ assert page.has_content? I18n.t('destroy_my_account')
+ assert page.has_no_css? '#update_login_and_password'
+ assert page.has_css? '#update_pgp_key'
+ end
+ end
+
+ test "change password" do
+ with_config user_actions: ['change_password'] do
+ username, password = submit_signup
+ click_on "Account Settings"
+ within('#update_login_and_password') do
+ fill_in 'Password', with: "other password"
+ fill_in 'Password confirmation', with: "other password"
+ click_on 'Save'
+ end
+ click_on 'Logout'
+ attempt_login(username, "other password")
+ assert page.has_content?("Welcome #{username}")
+ User.find_by_login(username).account.destroy
end
- click_on 'Logout'
- click_on 'Log In'
- fill_in 'Username', with: username
- fill_in 'Password', with: "other password"
- click_on 'Log In'
- assert page.has_content?("Welcome #{username}")
- User.find_by_login(username).account.destroy
end
test "change pgp key" do
- pgp_key = "My PGP Key Stub"
- username, password = submit_signup
- click_on "Account Settings"
- within('#update_pgp_key') do
- fill_in 'Public key', with: pgp_key
- click_on 'Save'
+ with_config user_actions: ['change_pgp_key'] do
+ pgp_key = FactoryGirl.build :pgp_key
+ username, password = submit_signup
+ click_on "Account Settings"
+ within('#update_pgp_key') do
+ fill_in 'Public key', with: pgp_key
+ click_on 'Save'
+ end
+ page.assert_selector 'input[value="Saving..."]'
+ # at some point we're done:
+ page.assert_no_selector 'input[value="Saving..."]'
+ assert page.has_field? 'Public key', with: pgp_key.to_s
+ user = User.find_by_login(username)
+ assert_equal pgp_key, user.public_key
+ user.account.destroy
end
- page.assert_selector 'input[value="Saving..."]'
- # at some point we're done:
- page.assert_no_selector 'input[value="Saving..."]'
- assert page.has_field? 'Public key', with: pgp_key
- user = User.find_by_login(username)
- assert_equal pgp_key, user.public_key
- user.account.destroy
end
@@ -81,6 +124,19 @@ class AccountTest < BrowserIntegrationTest
assert page.has_content?("server failed")
end
+ def attempt_login(username, password)
+ click_on 'Log In'
+ fill_in 'Username', with: username
+ fill_in 'Password', with: password
+ click_on 'Log In'
+ end
+
+ def assert_invalid_login(page)
+ assert page.has_selector? 'input.btn-primary.disabled'
+ assert page.has_content? I18n.t(:invalid_user_pass)
+ assert page.has_no_selector? 'input.btn-primary.disabled'
+ end
+
def inject_malicious_js
page.execute_script <<-EOJS
var calc = new srp.Calculate();
diff --git a/users/test/support/auth_test_helper.rb b/users/test/support/auth_test_helper.rb
index 609f115..50e9453 100644
--- a/users/test/support/auth_test_helper.rb
+++ b/users/test/support/auth_test_helper.rb
@@ -38,12 +38,26 @@ module AuthTestHelper
end
end
+ def expect_logout
+ expect_warden_logout
+ @token.expects(:destroy) if @token
+ end
+
protected
def header_for_token_auth
@token = find_record(:token, :authenticate => @current_user)
ActionController::HttpAuthentication::Token.encode_credentials @token.id
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
class ActionController::TestCase
diff --git a/users/test/support/integration_test_helper.rb b/users/test/support/integration_test_helper.rb
index cfe72cf..51e47c6 100644
--- a/users/test/support/integration_test_helper.rb
+++ b/users/test/support/integration_test_helper.rb
@@ -1,7 +1,7 @@
module IntegrationTestHelper
- def submit_signup
- username = "test_#{SecureRandom.urlsafe_base64}".downcase
- password = SecureRandom.base64
+ def submit_signup(username = nil, password = nil)
+ username ||= "test_#{SecureRandom.urlsafe_base64}".downcase
+ password ||= SecureRandom.base64
visit '/users/new'
fill_in 'Username', with: username
fill_in 'Password', with: password
diff --git a/users/test/unit/account_test.rb b/users/test/unit/account_test.rb
index 94a9980..4fb3c3d 100644
--- a/users/test/unit/account_test.rb
+++ b/users/test/unit/account_test.rb
@@ -2,6 +2,10 @@ require 'test_helper'
class AccountTest < ActiveSupport::TestCase
+ teardown do
+ Identity.destroy_all_disabled
+ end
+
test "create a new account" do
user = Account.create(FactoryGirl.attributes_for(:user))
assert user.valid?
@@ -13,7 +17,8 @@ class AccountTest < ActiveSupport::TestCase
end
test "create and remove a user account" do
- assert_no_difference "Identity.count" do
+ # We keep an identity that will block the handle from being reused.
+ assert_difference "Identity.count" do
assert_no_difference "User.count" do
user = Account.create(FactoryGirl.attributes_for(:user))
user.account.destroy
diff --git a/users/test/unit/identity_test.rb b/users/test/unit/identity_test.rb
index 0842a77..eca104f 100644
--- a/users/test/unit/identity_test.rb
+++ b/users/test/unit/identity_test.rb
@@ -90,6 +90,35 @@ class IdentityTest < ActiveSupport::TestCase
assert id.errors.messages[:destination].include? "needs to be a valid email address"
end
+ test "disabled identity" do
+ id = Identity.for(@user)
+ id.disable
+ assert_equal @user.email_address, id.address
+ assert_equal nil, id.destination
+ assert_equal nil, id.user
+ assert !id.enabled?
+ assert id.valid?
+ end
+
+ test "disabled identity blocks handle" do
+ id = Identity.for(@user)
+ id.disable
+ id.save
+ other_user = find_record :user
+ taken = Identity.build_for other_user, address: id.address
+ assert !taken.valid?
+ Identity.destroy_all_disabled
+ end
+
+ test "destroy all disabled identities" do
+ id = Identity.for(@user)
+ id.disable
+ id.save
+ assert Identity.count > 0
+ Identity.destroy_all_disabled
+ assert_equal 0, Identity.disabled.count
+ end
+
def alias_name
@alias_name ||= Faker::Internet.user_name
end
diff --git a/users/test/unit/local_email_test.rb b/users/test/unit/local_email_test.rb
index b25f46f..20ee7f1 100644
--- a/users/test/unit/local_email_test.rb
+++ b/users/test/unit/local_email_test.rb
@@ -24,6 +24,37 @@ class LocalEmailTest < ActiveSupport::TestCase
assert_equal ["needs to end in @#{LocalEmail.domain}"], local.errors[:email]
end
+ test "blacklists rfc2142" do
+ black_listed = LocalEmail.new('hostmaster')
+ assert !black_listed.valid?
+ end
+
+ test "blacklists etc passwd" do
+ black_listed = LocalEmail.new('nobody')
+ assert !black_listed.valid?
+ end
+
+ test "whitelist overwrites automatic blacklists" do
+ with_config handle_whitelist: ['nobody', 'hostmaster'] do
+ white_listed = LocalEmail.new('nobody')
+ assert white_listed.valid?
+ white_listed = LocalEmail.new('hostmaster')
+ assert white_listed.valid?
+ end
+ end
+
+ test "blacklists from config" do
+ black_listed = LocalEmail.new('www-data')
+ assert !black_listed.valid?
+ end
+
+ test "blacklist from config overwrites whitelist" do
+ with_config handle_whitelist: ['www-data'] do
+ black_listed = LocalEmail.new('www-data')
+ assert !black_listed.valid?
+ end
+ end
+
def handle
@handle ||= Faker::Internet.user_name
end
diff --git a/users/test/unit/token_test.rb b/users/test/unit/token_test.rb
index f56c576..6c9f209 100644
--- a/users/test/unit/token_test.rb
+++ b/users/test/unit/token_test.rb
@@ -7,9 +7,6 @@ class ClientCertificateTest < ActiveSupport::TestCase
@user = find_record :user
end
- teardown do
- end
-
test "new token for user" do
sample = Token.new(:user_id => @user.id)
assert sample.valid?
@@ -61,6 +58,26 @@ class ClientCertificateTest < ActiveSupport::TestCase
end
end
+ test "Token.destroy_all_expired is noop if no expiry is set" do
+ expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago
+ with_config auth: {} do
+ Token.destroy_all_expired
+ end
+ assert_equal expired, Token.find(expired.id)
+ end
+
+ test "Token.destroy_all_expired cleans up expired tokens only" do
+ expired = FactoryGirl.create :token, last_seen_at: 2.hours.ago
+ fresh = FactoryGirl.create :token
+ with_config auth: {token_expires_after: 60} do
+ Token.destroy_all_expired
+ end
+ assert_nil Token.find(expired.id)
+ assert_equal fresh, Token.find(fresh.id)
+ fresh.destroy
+ end
+
+
end