From 53808b073f539ba2b442738b6abf97228488e311 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 09:12:37 +0200 Subject: moving all of core into toplevel, tests fail. --- app/assets/javascripts/leap.js | 7 ++ app/assets/javascripts/platform.js | 93 +++++++++++++++++++++++++++ app/helpers/core_helper.rb | 13 ++++ app/helpers/download_helper.rb | 33 ++++++++++ app/helpers/navigation_helper.rb | 82 +++++++++++++++++++++++ app/views/common/_download_for_os.html.haml | 17 +++++ app/views/common/_home_page_buttons.html.haml | 23 +++++++ app/views/kaminari/_first_page.html.haml | 9 +++ app/views/kaminari/_gap.html.haml | 8 +++ app/views/kaminari/_last_page.html.haml | 9 +++ app/views/kaminari/_next_page.html.haml | 12 ++++ app/views/kaminari/_page.html.haml | 14 ++++ app/views/kaminari/_paginator.html.haml | 19 ++++++ app/views/kaminari/_prev_page.html.haml | 12 ++++ 14 files changed, 351 insertions(+) create mode 100644 app/assets/javascripts/leap.js create mode 100644 app/assets/javascripts/platform.js create mode 100644 app/helpers/core_helper.rb create mode 100644 app/helpers/download_helper.rb create mode 100644 app/helpers/navigation_helper.rb create mode 100644 app/views/common/_download_for_os.html.haml create mode 100644 app/views/common/_home_page_buttons.html.haml create mode 100644 app/views/kaminari/_first_page.html.haml create mode 100644 app/views/kaminari/_gap.html.haml create mode 100644 app/views/kaminari/_last_page.html.haml create mode 100644 app/views/kaminari/_next_page.html.haml create mode 100644 app/views/kaminari/_page.html.haml create mode 100644 app/views/kaminari/_paginator.html.haml create mode 100644 app/views/kaminari/_prev_page.html.haml (limited to 'app') diff --git a/app/assets/javascripts/leap.js b/app/assets/javascripts/leap.js new file mode 100644 index 0000000..94e602d --- /dev/null +++ b/app/assets/javascripts/leap.js @@ -0,0 +1,7 @@ + +// +// add a bootstrap alert to the page via javascript. +// +function alert_message(msg) { + $('#messages').append('
×'+msg+'
'); +} diff --git a/app/assets/javascripts/platform.js b/app/assets/javascripts/platform.js new file mode 100644 index 0000000..108c162 --- /dev/null +++ b/app/assets/javascripts/platform.js @@ -0,0 +1,93 @@ +/* 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) { + return 'linux'; + //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/app/helpers/core_helper.rb b/app/helpers/core_helper.rb new file mode 100644 index 0000000..a6c7479 --- /dev/null +++ b/app/helpers/core_helper.rb @@ -0,0 +1,13 @@ +# +# Misc. helpers needed throughout. +# +module CoreHelper + + # + # insert common buttons (download, login, etc) + # + def home_page_buttons(on_user_page = false) + render 'common/home_page_buttons', {:on_user_page => on_user_page} + end + +end diff --git a/app/helpers/download_helper.rb b/app/helpers/download_helper.rb new file mode 100644 index 0000000..ee0fe73 --- /dev/null +++ b/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/app/helpers/navigation_helper.rb b/app/helpers/navigation_helper.rb new file mode 100644 index 0000000..19cb934 --- /dev/null +++ b/app/helpers/navigation_helper.rb @@ -0,0 +1,82 @@ +module NavigationHelper + + # + # used to create a side navigation link. + # + # Signature is the same as link_to, except it accepts an :active value in the html_options + # + def link_to_navigation(*args) + if args.last.is_a? Hash + html_options = args.pop.dup + active_class = html_options.delete(:active) ? 'active' : nil + html_options[:class] = [html_options[:class], active_class].join(' ') + args << html_options + else + active_class = nil + end + content_tag :li, :class => active_class do + link_to(*args) + end + end + + # + # returns true if params[:action] matches one of the args. + # + def action?(*actions) + actions.detect do |action| + if action.is_a? String + action == action_string + elsif action.is_a? Symbol + if action == :none + action_string == nil + else + action == action_symbol + end + end + end + end + + # + # returns true if params[:controller] matches one of the args. + # + # for example: + # controller?(:me, :home) + # controller?('groups/') <-- matches any controller in namespace 'groups' + # + def controller?(*controllers) + controllers.each do |cntr| + if cntr.is_a? String + if cntr.ends_with?('/') + return true if controller_string.starts_with?(cntr.chop) + end + return true if cntr == controller_string + elsif cntr.is_a? Symbol + return true if cntr == controller_symbol + end + end + return false + end + + private + + def controller_string + @controller_string ||= params[:controller].to_s.gsub(/^\//, '') + end + + def controller_symbol + @controller_symbol ||= params[:controller].gsub(/^\//,'').gsub('/','_').to_sym + end + + def action_string + params[:action] + end + + def action_symbol + @action_symbol ||= if params[:action].present? + params[:action].to_sym + else + nil + end + end + +end diff --git a/app/views/common/_download_for_os.html.haml b/app/views/common/_download_for_os.html.haml new file mode 100644 index 0000000..3a11d10 --- /dev/null +++ b/app/views/common/_download_for_os.html.haml @@ -0,0 +1,17 @@ +- 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 + = big_icon('download') + .pull-right + = 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/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml new file mode 100644 index 0000000..c9ea7a2 --- /dev/null +++ b/app/views/common/_home_page_buttons.html.haml @@ -0,0 +1,23 @@ +- icon_color = :black + +.home-buttons + .row-fluid.first + .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] + - if !local_assigns[:on_user_page] + .row-fluid.second + .login.span4 + %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') + %span.info= t(:login_info) + .signup.span4 + %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') + %span.info= t(:signup_info) + .help.span4 + %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') + %span.info= t(:help_info) diff --git a/app/views/kaminari/_first_page.html.haml b/app/views/kaminari/_first_page.html.haml new file mode 100644 index 0000000..34436e3 --- /dev/null +++ b/app/views/kaminari/_first_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "First" page +-# available local variables +-# url: url to the first page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, :remote => remote diff --git a/app/views/kaminari/_gap.html.haml b/app/views/kaminari/_gap.html.haml new file mode 100644 index 0000000..51de678 --- /dev/null +++ b/app/views/kaminari/_gap.html.haml @@ -0,0 +1,8 @@ +-# Non-link tag that stands for skipped pages... +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li.disabled + = raw(t 'views.pagination.truncate') diff --git a/app/views/kaminari/_last_page.html.haml b/app/views/kaminari/_last_page.html.haml new file mode 100644 index 0000000..c90433c --- /dev/null +++ b/app/views/kaminari/_last_page.html.haml @@ -0,0 +1,9 @@ +-# Link to the "Last" page +-# available local variables +-# url: url to the last page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +%li + = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {:remote => remote} diff --git a/app/views/kaminari/_next_page.html.haml b/app/views/kaminari/_next_page.html.haml new file mode 100644 index 0000000..ea6cab2 --- /dev/null +++ b/app/views/kaminari/_next_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Next" page +-# available local variables +-# url: url to the next page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.last? + %li.disabled + %span= raw(t 'views.pagination.next') +- else + %li= link_to(raw(t 'views.pagination.next'), url, :rel => 'next', :remote => remote) diff --git a/app/views/kaminari/_page.html.haml b/app/views/kaminari/_page.html.haml new file mode 100644 index 0000000..2f2f142 --- /dev/null +++ b/app/views/kaminari/_page.html.haml @@ -0,0 +1,14 @@ +-# Link showing page number +-# available local variables +-# page: a page object for "this" page +-# url: url to this page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote + +- if page.current? + %li.active + %span= page +- else + %li= link_to(page, url, {:remote => remote, :rel => page.next? ? 'next' : page.prev? ? 'prev' : nil}) diff --git a/app/views/kaminari/_paginator.html.haml b/app/views/kaminari/_paginator.html.haml new file mode 100644 index 0000000..79c5b92 --- /dev/null +++ b/app/views/kaminari/_paginator.html.haml @@ -0,0 +1,19 @@ +-# The container tag +-# available local variables +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +-# paginator: the paginator that renders the pagination tags inside += paginator.render do + .pagination + %ul + -#= first_page_tag unless current_page.first? + = prev_page_tag #unless current_page.first? + - each_page do |page| + - if page.left_outer? || page.right_outer? || page.inside_window? + = page_tag page + - elsif !page.was_truncated? + = gap_tag + = next_page_tag #unless current_page.last? + -#= last_page_tag unless current_page.last? diff --git a/app/views/kaminari/_prev_page.html.haml b/app/views/kaminari/_prev_page.html.haml new file mode 100644 index 0000000..d274bf4 --- /dev/null +++ b/app/views/kaminari/_prev_page.html.haml @@ -0,0 +1,12 @@ +-# Link to the "Previous" page +-# available local variables +-# url: url to the previous page +-# current_page: a page object for the currently displayed page +-# num_pages: total number of pages +-# per_page: number of items to fetch per page +-# remote: data-remote +- if current_page.first? + %li.disabled + %span= raw(t 'views.pagination.previous') +- else + %li= link_to(raw(t 'views.pagination.previous'), url, :rel => 'prev', :remote => remote) -- cgit v1.2.3 From b6d14dc19dd350a807826e3e097738a36613e083 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 8 Apr 2014 11:49:14 +0200 Subject: moving users: app and test files --- app/assets/images/leap_web_users/.gitkeep | 0 app/assets/javascripts/leap_web_users/.gitkeep | 0 app/assets/javascripts/users.js | 132 +++++++++++++++ app/assets/stylesheets/leap_web_users/.gitkeep | 0 app/controllers/.gitkeep | 0 app/controllers/account_settings_controller.rb | 0 .../controller_extension/authentication.rb | 75 +++++++++ .../controller_extension/token_authentication.rb | 27 ++++ app/controllers/keys_controller.rb | 18 +++ app/controllers/sessions_controller.rb | 28 ++++ app/controllers/users_base_controller.rb | 18 +++ app/controllers/users_controller.rb | 69 ++++++++ app/controllers/v1/messages_controller.rb | 25 +++ app/controllers/v1/sessions_controller.rb | 45 ++++++ app/controllers/v1/users_controller.rb | 32 ++++ app/controllers/webfinger_controller.rb | 19 +++ app/designs/message/by_user_ids_to_show.js | 7 + .../message/by_user_ids_to_show_and_created_at.js | 9 ++ ...by_created_at_and_one_month_warning_not_sent.js | 5 + app/helpers/.gitkeep | 0 app/helpers/email_aliases_helper.rb | 11 ++ app/helpers/sessions_helper.rb | 2 + app/helpers/users_helper.rb | 14 ++ app/models/account.rb | 68 ++++++++ app/models/email.rb | 26 +++ app/models/identity.rb | 136 ++++++++++++++++ app/models/local_email.rb | 68 ++++++++ app/models/login_format_validation.rb | 21 +++ app/models/message.rb | 29 ++++ app/models/pgp_key.rb | 48 ++++++ app/models/service_level.rb | 19 +++ app/models/session.rb | 32 ++++ app/models/token.rb | 69 ++++++++ app/models/unauthenticated_user.rb | 6 + app/models/user.rb | 179 +++++++++++++++++++++ app/views/.gitkeep | 0 app/views/emails/_email.html.haml | 6 + app/views/sessions/new.html.haml | 10 ++ app/views/sessions/new.json.erb | 3 + app/views/users/_change_password.html.haml | 21 +++ app/views/users/_change_pgp_key.html.haml | 13 ++ app/views/users/_change_service_level.html.haml | 18 +++ app/views/users/_destroy_account.html.haml | 27 ++++ app/views/users/_edit.html.haml | 14 ++ app/views/users/_user.html.haml | 4 + app/views/users/_warnings.html.haml | 12 ++ app/views/users/edit.html.haml | 1 + app/views/users/index.html.haml | 13 ++ app/views/users/new.html.haml | 19 +++ app/views/users/show.html.haml | 25 +++ app/views/v1/sessions/new.json.erb | 3 + app/views/webfinger/host_meta.xml.erb | 11 ++ app/views/webfinger/search.xml.erb | 7 + 53 files changed, 1444 insertions(+) create mode 100644 app/assets/images/leap_web_users/.gitkeep create mode 100644 app/assets/javascripts/leap_web_users/.gitkeep create mode 100644 app/assets/javascripts/users.js create mode 100644 app/assets/stylesheets/leap_web_users/.gitkeep create mode 100644 app/controllers/.gitkeep create mode 100644 app/controllers/account_settings_controller.rb create mode 100644 app/controllers/controller_extension/authentication.rb create mode 100644 app/controllers/controller_extension/token_authentication.rb create mode 100644 app/controllers/keys_controller.rb create mode 100644 app/controllers/sessions_controller.rb create mode 100644 app/controllers/users_base_controller.rb create mode 100644 app/controllers/users_controller.rb create mode 100644 app/controllers/v1/messages_controller.rb create mode 100644 app/controllers/v1/sessions_controller.rb create mode 100644 app/controllers/v1/users_controller.rb create mode 100644 app/controllers/webfinger_controller.rb create mode 100644 app/designs/message/by_user_ids_to_show.js create mode 100644 app/designs/message/by_user_ids_to_show_and_created_at.js create mode 100644 app/designs/user/by_created_at_and_one_month_warning_not_sent.js create mode 100644 app/helpers/.gitkeep create mode 100644 app/helpers/email_aliases_helper.rb create mode 100644 app/helpers/sessions_helper.rb create mode 100644 app/helpers/users_helper.rb create mode 100644 app/models/account.rb create mode 100644 app/models/email.rb create mode 100644 app/models/identity.rb create mode 100644 app/models/local_email.rb create mode 100644 app/models/login_format_validation.rb create mode 100644 app/models/message.rb create mode 100644 app/models/pgp_key.rb create mode 100644 app/models/service_level.rb create mode 100644 app/models/session.rb create mode 100644 app/models/token.rb create mode 100644 app/models/unauthenticated_user.rb create mode 100644 app/models/user.rb create mode 100644 app/views/.gitkeep create mode 100644 app/views/emails/_email.html.haml create mode 100644 app/views/sessions/new.html.haml create mode 100644 app/views/sessions/new.json.erb create mode 100644 app/views/users/_change_password.html.haml create mode 100644 app/views/users/_change_pgp_key.html.haml create mode 100644 app/views/users/_change_service_level.html.haml create mode 100644 app/views/users/_destroy_account.html.haml create mode 100644 app/views/users/_edit.html.haml create mode 100644 app/views/users/_user.html.haml create mode 100644 app/views/users/_warnings.html.haml create mode 100644 app/views/users/edit.html.haml create mode 100644 app/views/users/index.html.haml create mode 100644 app/views/users/new.html.haml create mode 100644 app/views/users/show.html.haml create mode 100644 app/views/v1/sessions/new.json.erb create mode 100644 app/views/webfinger/host_meta.xml.erb create mode 100644 app/views/webfinger/search.xml.erb (limited to 'app') diff --git a/app/assets/images/leap_web_users/.gitkeep b/app/assets/images/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/leap_web_users/.gitkeep b/app/assets/javascripts/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js new file mode 100644 index 0000000..8486756 --- /dev/null +++ b/app/assets/javascripts/users.js @@ -0,0 +1,132 @@ +(function() { + // + // LOCAL FUNCTIONS + // + + var poll_users, + prevent_default, + form_failed, + form_passed, + clear_errors, + update_user; + + prevent_default = function(event) { + return event.preventDefault(); + }; + + poll_users = function(query, process) { + return $.get("/1/users.json", { + query: query + }).done(process); + }; + + clear_errors = function() { + return $('#messages').empty(); + }; + + update_user = function(submitEvent) { + var form = submitEvent.target; + var token = form.dataset.token; + var url = form.action; + var req = $.ajax({ + url: url, + type: 'PUT', + headers: { Authorization: 'Token token="' + token + '"' }, + data: $(form).serialize() + }); + req.done( function() { + $(form).find('input[type="submit"]').button('reset'); + }); + }; + + markAsSubmitted = function(submitEvent) { + var form = submitEvent.target; + $(form).addClass('submitted') + // bootstrap loading state: + $(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 + // + + srp.session = new srp.Session(); + + srp.signedUp = function() { + return srp.login(); + }; + + srp.loggedIn = function() { + return window.location = '/'; + }; + + srp.updated = function() { + return window.location = '/users/' + srp.session.id(); + }; + + // + // if a json request returns an error, this function gets called and + // decorates the appropriate fields with the error messages. + // + srp.error = function(message) { + clear_errors(); + var errors = extractErrors(message); + displayErrors(errors); + resetButtons(); + } + + function extractErrors(message) { + if ($.isPlainObject(message) && message.errors) { + return message.errors; + } else { + 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); + } + }; + + // + // INIT + // + + $(document).ready(function() { + $('form').submit(markAsSubmitted); + $('#new_user').submit(prevent_default); + $('#new_user').submit(srp.signup); + $('#new_session').submit(prevent_default); + $('#new_session').submit(srp.login); + $('#update_login_and_password').submit(prevent_default); + $('#update_login_and_password').submit(srp.update); + $('#update_pgp_key').submit(prevent_default); + $('#update_pgp_key').submit(update_user); + return $('#user-typeahead').typeahead({ + source: poll_users + }); + }); + +}).call(this); diff --git a/app/assets/stylesheets/leap_web_users/.gitkeep b/app/assets/stylesheets/leap_web_users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/.gitkeep b/app/controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/account_settings_controller.rb b/app/controllers/account_settings_controller.rb new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb new file mode 100644 index 0000000..03d3989 --- /dev/null +++ b/app/controllers/controller_extension/authentication.rb @@ -0,0 +1,75 @@ +module ControllerExtension::Authentication + extend ActiveSupport::Concern + + private + + included do + helper_method :current_user, :logged_in?, :admin? + end + + def current_user + @current_user ||= token_authenticate || warden.user + end + + def logged_in? + !!current_user + end + + def require_login + access_denied unless logged_in? + end + + # some actions only make sense if you are not logged in yet. + # (login, signup). If a user tries to perform these they will + # be redirected to their dashboard. + def redirect_if_logged_in + redirect_to home_url if logged_in? + end + + def access_denied + respond_to do |format| + format.html do + if logged_in? + redirect_to home_url, :alert => t(:not_authorized) + else + redirect_to login_url, :alert => t(:not_authorized_login) + end + end + format.json do + render :json => {'error' => t(:not_authorized)}, status: :unprocessable_entity + end + end + end + + def admin? + current_user && current_user.is_admin? + end + + def require_admin + access_denied unless admin? + end + + def authentication_errors + return unless attempted_login? + errors = get_warden_errors + errors.inject({}) do |translated,err| + translated[err.first] = I18n.t(err.last) + translated + end + end + + def get_warden_errors + if strategy = warden.winning_strategy + message = strategy.message + # in case we get back the default message to fail! + message.respond_to?(:inject) ? message : { base: message } + else + { login: :all_strategies_failed } + end + end + + def attempted_login? + request.env['warden.options'] && + request.env['warden.options'][:attempted_path] + end +end diff --git a/app/controllers/controller_extension/token_authentication.rb b/app/controllers/controller_extension/token_authentication.rb new file mode 100644 index 0000000..6e0a6ce --- /dev/null +++ b/app/controllers/controller_extension/token_authentication.rb @@ -0,0 +1,27 @@ +module ControllerExtension::TokenAuthentication + extend ActiveSupport::Concern + + def token + @token ||= authenticate_with_http_token do |token_id, options| + Token.find(token_id) + end + end + + def token_authenticate + @token_authenticated ||= token.authenticate if token + end + + def require_token + access_denied unless token_authenticate + end + + def logout + super + clear_token + end + + def clear_token + token.destroy if token + end +end + diff --git a/app/controllers/keys_controller.rb b/app/controllers/keys_controller.rb new file mode 100644 index 0000000..fb28901 --- /dev/null +++ b/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/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..8919a4d --- /dev/null +++ b/app/controllers/sessions_controller.rb @@ -0,0 +1,28 @@ +class SessionsController < ApplicationController + + before_filter :redirect_if_logged_in, :only => [:new] + + def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end + end + + def destroy + logout + redirect_to home_url + end + + # + # 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}" + # throw :warden, response.finish + #end + +end diff --git a/app/controllers/users_base_controller.rb b/app/controllers/users_base_controller.rb new file mode 100644 index 0000000..9becf0d --- /dev/null +++ b/app/controllers/users_base_controller.rb @@ -0,0 +1,18 @@ +# +# common base class for all user related controllers +# + +class UsersBaseController < ApplicationController + + protected + + def fetch_user + @user = User.find(params[:user_id] || params[:id]) + if !@user && admin? + redirect_to users_url, :alert => t(:no_such_thing, :thing => 'user') + elsif !admin? && @user != current_user + access_denied + end + end + +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..c8e09b6 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,69 @@ +# +# This is an HTML-only controller. For the JSON-only controller, see v1/users_controller.rb +# + +class UsersController < UsersBaseController + + before_filter :require_login, :except => [:new] + before_filter :redirect_if_logged_in, :only => [:new] + before_filter :require_admin, :only => [:index, :deactivate, :enable] + before_filter :fetch_user, :only => [:show, :edit, :update, :destroy, :deactivate, :enable] + + respond_to :html + + def index + if params[:query] + if @user = User.find_by_login(params[:query]) + redirect_to @user + return + else + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + end + else + @users = User.by_created_at.descending + end + @users = @users.limit(100) + end + + def new + @user = User.new + end + + def show + end + + 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 + respond_with @user + end + + def enable + @user.enabled = true + @user.save + respond_with @user + end + + def destroy + @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 bye_url + end + end + +end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb new file mode 100644 index 0000000..f71d0f1 --- /dev/null +++ b/app/controllers/v1/messages_controller.rb @@ -0,0 +1,25 @@ +module V1 + class MessagesController < ApplicationController + + skip_before_filter :verify_authenticity_token + before_filter :require_token + + respond_to :json + + def index + render json: (current_user ? current_user.messages : [] ) + end + + def update + message = Message.find(params[:id]) + if (message and current_user) + message.mark_as_read_by(current_user) + message.save + render json: true + else + render json: false + end + end + + end +end diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb new file mode 100644 index 0000000..eae3a1e --- /dev/null +++ b/app/controllers/v1/sessions_controller.rb @@ -0,0 +1,45 @@ +module V1 + class SessionsController < ApplicationController + + skip_before_filter :verify_authenticity_token + before_filter :require_token, only: :destroy + + def new + @session = Session.new + if authentication_errors + @errors = authentication_errors + render :status => 422 + end + end + + def create + logout if logged_in? + if params['A'] + authenticate! + else + @user = User.find_by_login(params['login']) + render :json => {salt: @user.salt} + end + end + + def update + authenticate! + @token = Token.create(:user_id => current_user.id) + session[:token] = @token.id + render :json => login_response + end + + def destroy + logout + head :no_content + end + + protected + + def login_response + handshake = session.delete(:handshake) || {} + handshake.to_hash.merge(:id => current_user.id, :token => @token.id) + end + + end +end diff --git a/app/controllers/v1/users_controller.rb b/app/controllers/v1/users_controller.rb new file mode 100644 index 0000000..8897d01 --- /dev/null +++ b/app/controllers/v1/users_controller.rb @@ -0,0 +1,32 @@ +module V1 + class UsersController < UsersBaseController + + skip_before_filter :verify_authenticity_token + before_filter :fetch_user, :only => [:update] + before_filter :require_admin, :only => [:index] + before_filter :require_token, :only => [:update] + + respond_to :json + + # used for autocomplete for admins in the web ui + def index + if params[:query] + @users = User.by_login.startkey(params[:query]).endkey(params[:query].succ) + respond_with @users.map(&:login).sort + else + render :json => {'error' => 'query required', 'status' => :unprocessable_entity} + end + end + + def create + @user = Account.create(params[:user]) + respond_with @user # return ID instead? + end + + def update + @user.account.update params[:user] + respond_with @user + end + + end +end diff --git a/app/controllers/webfinger_controller.rb b/app/controllers/webfinger_controller.rb new file mode 100644 index 0000000..8872802 --- /dev/null +++ b/app/controllers/webfinger_controller.rb @@ -0,0 +1,19 @@ +class WebfingerController < ApplicationController + + respond_to :xml, :json + layout false + + def host_meta + @host_meta = Webfinger::HostMetaPresenter.new(request) + respond_with @host_meta + end + + def search + username = params[:q].split('@')[0].to_s.downcase + user = User.find_by_login(username) + raise RECORD_NOT_FOUND, 'User not found' unless user.present? + @presenter = Webfinger::UserPresenter.new(user, request) + respond_with @presenter + end + +end diff --git a/app/designs/message/by_user_ids_to_show.js b/app/designs/message/by_user_ids_to_show.js new file mode 100644 index 0000000..e33566b --- /dev/null +++ b/app/designs/message/by_user_ids_to_show.js @@ -0,0 +1,7 @@ +function (doc) { + if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { + doc.user_ids_to_show.forEach(function (userId) { + emit(userId, 1); + }); + } +} diff --git a/app/designs/message/by_user_ids_to_show_and_created_at.js b/app/designs/message/by_user_ids_to_show_and_created_at.js new file mode 100644 index 0000000..54e4604 --- /dev/null +++ b/app/designs/message/by_user_ids_to_show_and_created_at.js @@ -0,0 +1,9 @@ +// not using at moment +// call with something like Message.by_user_ids_to_show_and_created_at.startkey([user_id, start_date]).endkey([user_id,end_date]) +function (doc) { + if (doc.type === 'Message' && doc.user_ids_to_show && Array.isArray(doc.user_ids_to_show)) { + doc.user_ids_to_show.forEach(function (userId) { + emit([userId, doc.created_at], 1); + }); + } +} diff --git a/app/designs/user/by_created_at_and_one_month_warning_not_sent.js b/app/designs/user/by_created_at_and_one_month_warning_not_sent.js new file mode 100644 index 0000000..53a95de --- /dev/null +++ b/app/designs/user/by_created_at_and_one_month_warning_not_sent.js @@ -0,0 +1,5 @@ +function (doc) { + if ((doc['type'] == 'User') && (doc['created_at'] != null) && (doc['one_month_warning_sent'] == null)) { + emit(doc['created_at'], 1); + } +} diff --git a/app/helpers/.gitkeep b/app/helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/helpers/email_aliases_helper.rb b/app/helpers/email_aliases_helper.rb new file mode 100644 index 0000000..b56b068 --- /dev/null +++ b/app/helpers/email_aliases_helper.rb @@ -0,0 +1,11 @@ +module EmailAliasesHelper + + def email_alias_form(options = {}) + simple_form_for [@user, EmailAlias.new()], + :html => {:class => "form-horizontal email-alias form"}, + :validate => true do |f| + yield f + end + end + +end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..309f8b2 --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..f56faab --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,14 @@ +module UsersHelper + + def user_form_class(*classes) + (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') + end + + def wrapped(item, options = {}) + options[:as] ||= :div + content_tag options[:as], :class => dom_class(item), :id => dom_id(item) do + yield + end + end + +end diff --git a/app/models/account.rb b/app/models/account.rb new file mode 100644 index 0000000..cf998e4 --- /dev/null +++ b/app/models/account.rb @@ -0,0 +1,68 @@ +# +# 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 + + attr_reader :user + + def initialize(user = nil) + @user = user + end + + # Returns the user record so it can be used in views. + def self.create(attrs) + @user = User.create(attrs).tap do |user| + Identity.create_for user + end + end + + def update(attrs) + if attrs[:password_verifier].present? + update_login(attrs[:login]) + @user.update_attributes attrs.slice(:password_verifier, :password_salt) + end + # TODO: move into identity controller + 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.disable_all_for(@user) + @user.destroy + end + + protected + + def update_login(login) + return unless login.present? + @old_identity = Identity.for(@user) + @user.login = login + @new_identity = Identity.for(@user) # based on the new login + @old_identity.destination = @user.email_address # alias old -> new + end + + def update_pgp_key(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/app/models/email.rb b/app/models/email.rb new file mode 100644 index 0000000..a9a503f --- /dev/null +++ b/app/models/email.rb @@ -0,0 +1,26 @@ +class Email < String + include ActiveModel::Validations + + validates :email, + :format => { + :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/, #local part of email is case-sensitive, so allow uppercase letter. + :message => "needs to be a valid email address" + } + + def to_partial_path + "emails/email" + end + + def to_param + to_s + end + + def email + self + end + + def handle + self.split('@').first + end + +end diff --git a/app/models/identity.rb b/app/models/identity.rb new file mode 100644 index 0000000..9b97b51 --- /dev/null +++ b/app/models/identity.rb @@ -0,0 +1,136 @@ +class Identity < CouchRest::Model::Base + include LoginFormatValidation + + use_database :identities + + belongs_to :user + + property :address, LocalEmail + property :destination, Email + property :keys, HashWithIndifferentAccess + + validate :unique_forward + validate :alias_available + validate :address_local_email + validate :destination_email + + design do + view :by_user_id + view :by_address_and_destination + view :by_address + view :pgp_key_by_email, + map: <<-EOJS + function(doc) { + if (doc.type != 'Identity') { + return; + } + if (typeof doc.keys === "object") { + 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 + + def self.for(user, attributes = {}) + find_for(user, attributes) || build_for(user, attributes) + end + + def self.find_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + find_by_address_and_destination [attributes[:address], attributes[:destination]] + end + + def self.build_for(user, attributes = {}) + attributes.reverse_merge! attributes_from_user(user) + Identity.new(attributes) + end + + def self.create_for(user, attributes = {}) + identity = build_for(user, attributes) + identity.save + 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, + destination: user.email_address + } + 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, key) + return if keys[type] == key.to_s + write_attribute('keys', keys.merge(type => key.to_s)) + end + + # for LoginFormatValidation + def login + self.address.handle + end + + protected + + def unique_forward + same = Identity.find_by_address_and_destination([address, destination]) + if same && same != self + errors.add :base, "This alias already exists" + end + end + + def alias_available + same = Identity.find_by_address(address) + if same && same.user != self.user + errors.add :base, "This email has already been taken" + end + end + + def address_local_email + return if address.valid? #this ensures it is LocalEmail + self.errors.add(:address, address.errors.messages[:email].first) #assumes only one error + end + + def destination_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 + +end diff --git a/app/models/local_email.rb b/app/models/local_email.rb new file mode 100644 index 0000000..2b4c65e --- /dev/null +++ b/app/models/local_email.rb @@ -0,0 +1,68 @@ +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] + end + + validates :email, + :format => { + :with => /@#{domain}\Z/i, + :message => "needs to end in @#{domain}" + } + + validate :handle_allowed + + def initialize(s) + super + append_domain_if_needed + end + + def to_key + [handle] + end + + def domain + LocalEmail.domain + end + + protected + + def append_domain_if_needed + unless self.index('@') + self << '@' + domain + 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/app/models/login_format_validation.rb b/app/models/login_format_validation.rb new file mode 100644 index 0000000..c1fcf70 --- /dev/null +++ b/app/models/login_format_validation.rb @@ -0,0 +1,21 @@ +module LoginFormatValidation + extend ActiveSupport::Concern + + #TODO: Probably will replace this. Playing with using it for aliases too, but won't want it connected to login field. + + included do + # Have multiple regular expression validations so we can get specific error messages: + validates :login, + :format => { :with => /\A.{2,}\z/, + :message => "Must have at least two characters"} + validates :login, + :format => { :with => /\A[a-z\d_\.-]+\z/, + :message => "Only lowercase letters, digits, . - and _ allowed."} + validates :login, + :format => { :with => /\A[a-z].*\z/, + :message => "Must begin with a lowercase letter"} + validates :login, + :format => { :with => /\A.*[a-z\d]\z/, + :message => "Must end with a letter or digit"} + end +end diff --git a/app/models/message.rb b/app/models/message.rb new file mode 100644 index 0000000..424f094 --- /dev/null +++ b/app/models/message.rb @@ -0,0 +1,29 @@ +class Message < CouchRest::Model::Base + + use_database :messages + + property :text, String + property :user_ids_to_show, [String] + property :user_ids_have_shown, [String] # is this necessary to store? + + timestamps! + + design do + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'message')) + end + + def mark_as_read_by(user) + user_ids_to_show.delete(user.id) + # is it necessary to keep track of what users have already seen it? + user_ids_have_shown << user.id unless read_by?(user) + end + + def read_by?(user) + user_ids_have_shown.include?(user.id) + end + + def unread_by?(user) + user_ids_to_show.include?(user.id) + end +end diff --git a/app/models/pgp_key.rb b/app/models/pgp_key.rb new file mode 100644 index 0000000..66f8660 --- /dev/null +++ b/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/app/models/service_level.rb b/app/models/service_level.rb new file mode 100644 index 0000000..299aaf1 --- /dev/null +++ b/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/app/models/session.rb b/app/models/session.rb new file mode 100644 index 0000000..0d7e10e --- /dev/null +++ b/app/models/session.rb @@ -0,0 +1,32 @@ +class Session < SRP::Session + include ActiveModel::Validations + include LoginFormatValidation + + attr_accessor :login + + validates :login, :presence => true + + def initialize(user = nil, aa = nil) + super(user, aa) if user + end + + def persisted? + false + end + + def new_record? + true + end + + def to_model + self + end + + def to_key + [object_id] + end + + def to_param + nil + end +end diff --git a/app/models/token.rb b/app/models/token.rb new file mode 100644 index 0000000..4856c31 --- /dev/null +++ b/app/models/token.rb @@ -0,0 +1,69 @@ +class Token < CouchRest::Model::Base + + use_database :tokens + + belongs_to :user + + # timestamps! does not create setters and only sets updated_at + # if the object has changed and been saved. Instead of triggering + # that we rather use our own property we have control over: + property :last_seen_at, Time, accessible: false + + 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 + return nil + else + touch + return user + end + end + + # Tokens can be cleaned up in different ways. + # So let's make sure we don't crash if they disappeared + def destroy_with_rescue + destroy_without_rescue + rescue RestClient::ResourceNotFound + end + alias_method_chain :destroy, :rescue + + def touch + self.last_seen_at = Time.now + save + end + + def expired? + Token.expires_after and + last_seen_at < Token.expires_after.minutes.ago + end + + def initialize(*args) + super + if new_record? + self.id = SecureRandom.urlsafe_base64(32).gsub(/^_*/, '') + self.last_seen_at = Time.now + end + end +end + diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb new file mode 100644 index 0000000..0fc17d2 --- /dev/null +++ b/app/models/unauthenticated_user.rb @@ -0,0 +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/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c297ac8 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,179 @@ +class User < CouchRest::Model::Base + include LoginFormatValidation + + use_database :users + + property :login, String, :accessible => true + property :password_verifier, String, :accessible => true + property :password_salt, String, :accessible => true + + 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 + + property :one_month_warning_sent, TrueClass + + before_save :update_effective_service_level + + validates :login, :password_salt, :password_verifier, + :presence => true + + validates :login, + :uniqueness => true, + :if => :serverside? + + validate :login_is_unique_alias + + validates :password_salt, :password_verifier, + :format => { :with => /\A[\dA-Fa-f]+\z/, :message => "Only hex numbers allowed" } + + validates :password, :presence => true, + :confirmation => true, + :format => { :with => /.{8}.*/, :message => "needs to be at least 8 characters long" } + + timestamps! + + design do + own_path = Pathname.new(File.dirname(__FILE__)) + load_views(own_path.join('..', 'designs', 'user')) + view :by_login + view :by_created_at + end # end of design + + def to_json(options={}) + { + :login => login, + :ok => valid? + }.to_json(options) + end + + def salt + password_salt.hex + end + + def verifier + password_verifier.hex + end + + def username + login + end + + def email_address + LocalEmail.new(login) + end + + # Since we are storing admins by login, we cannot allow admins to change their login. + def is_admin? + APP_CONFIG['admins'].include? self.login + end + + def most_recent_tickets(count=3) + Ticket.for_user(self).limit(count).all #defaults to having most recent updated first + end + + def messages(unseen = true) + #TODO for now this only shows unseen messages. Will we ever want seen ones? Is it necessary to store? + + # we don't want to emit all the userids associated with a message, so only emit id and text. + Message.by_user_ids_to_show.key(self.id).map { |message| [message.id, message.text] } + + end + + # DEPRECATED + # + # Please set the key on the identity directly + # WARNING: This will not be serialized with the user record! + # It is only a workaround for the key form. + def public_key=(value) + identity.set_key(:pgp, value) + end + + # DEPRECATED + # + # Please access identity.keys[:pgp] directly + def public_key + identity.keys[:pgp] + end + + def account + Account.new(self) + end + + def identity + @identity ||= Identity.for(self) + end + + def refresh_identity + @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 + + + def self.send_one_month_warnings + + # To determine warnings to send, need to get all users where one_month_warning_sent is not set, and where it was created greater than or equal to 1 month ago. + # TODO: might want to further limit to enabled accounts, and, based on provider's service level configuration, for particular service levels. + users_to_warn = User.by_created_at_and_one_month_warning_not_sent.endkey(Time.now-1.month) + + users_to_warn.each do |user| + # instead of loop could use something like: + # message.user_ids_to_show = users_to_warn.map(&:id) + # but would still need to loop through users to store one_month_warning_sent + + if !@message + # create a message for today's date + # only want to create once, and only if it will be used. + @message = Message.new(:text => I18n.t(:payment_one_month_warning, :date_in_one_month => (Time.now+1.month).strftime("%Y-%d-%m"))) + end + + @message.user_ids_to_show << user.id + user.one_month_warning_sent = true + user.save + end + @message.save if @message + + end + + protected + + ## + # Validation Functions + ## + + def login_is_unique_alias + alias_identity = Identity.find_by_address(self.email_address) + return if alias_identity.blank? + if alias_identity.user != self + errors.add(:login, "has already been taken") + end + end + + def password + password_verifier + end + + # used as a condition for validations that are server side only + 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/app/views/.gitkeep b/app/views/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/views/emails/_email.html.haml b/app/views/emails/_email.html.haml new file mode 100644 index 0000000..ea59cec --- /dev/null +++ b/app/views/emails/_email.html.haml @@ -0,0 +1,6 @@ += wrapped(email, local_assigns) do + = email + - if local_assigns[:with].try(:include?, :delete) + = link_to(user_email_alias_path(@user, email), :method => :delete) do + %i.icon-remove + diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml new file mode 100644 index 0000000..771dc97 --- /dev/null +++ b/app/views/sessions/new.html.haml @@ -0,0 +1,10 @@ +.span1 +.span9 + = render :partial => 'users/warnings' + %h2=t :login + = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| + = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } + = f.input :password, :required => false, :input_html => { :id => :srp_password } + .form-actions + = f.button :submit, :value => t(:login), :class => 'btn-primary' + = link_to t(:cancel), home_path, :class => 'btn' diff --git a/app/views/sessions/new.json.erb b/app/views/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/app/views/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/app/views/users/_change_password.html.haml b/app/views/users/_change_password.html.haml new file mode 100644 index 0000000..425e3ee --- /dev/null +++ b/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/app/views/users/_change_pgp_key.html.haml b/app/views/users/_change_pgp_key.html.haml new file mode 100644 index 0000000..e465125 --- /dev/null +++ b/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/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml new file mode 100644 index 0000000..61e67d9 --- /dev/null +++ b/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/app/views/users/_destroy_account.html.haml b/app/views/users/_destroy_account.html.haml new file mode 100644 index 0000000..445f3c4 --- /dev/null +++ b/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/app/views/users/_edit.html.haml b/app/views/users/_edit.html.haml new file mode 100644 index 0000000..1d2b68a --- /dev/null +++ b/app/views/users/_edit.html.haml @@ -0,0 +1,14 @@ +-# +-# edit user form, used by both show and edit actions. +-# +-# 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/app/views/users/_user.html.haml b/app/views/users/_user.html.haml new file mode 100644 index 0000000..583d22f --- /dev/null +++ b/app/views/users/_user.html.haml @@ -0,0 +1,4 @@ +%tr + %td= link_to user.login, user + %td= l(user.created_at, :format => :short) + %td= l(user.updated_at, :format => :short) diff --git a/app/views/users/_warnings.html.haml b/app/views/users/_warnings.html.haml new file mode 100644 index 0000000..79ab103 --- /dev/null +++ b/app/views/users/_warnings.html.haml @@ -0,0 +1,12 @@ +%noscript + %div.alert.alert-error=t :js_required_html +#cookie_warning.alert.alert-error{:style => "display:none"} + =t :cookie_disabled_warning +:javascript + document.cookie = "testing=cookies_enabled; path=/"; + if(document.cookie.indexOf("testing=cookies_enabled") < 0) + { + document.getElementById('cookie_warning').style.display = 'block'; + } else { + document.getElementById('cookie_warning').style.display = 'none'; + } \ No newline at end of file diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml new file mode 100644 index 0000000..434c025 --- /dev/null +++ b/app/views/users/edit.html.haml @@ -0,0 +1 @@ += render 'edit' diff --git a/app/views/users/index.html.haml b/app/views/users/index.html.haml new file mode 100644 index 0000000..fc1001e --- /dev/null +++ b/app/views/users/index.html.haml @@ -0,0 +1,13 @@ +- @show_navigation = false + += form_tag users_path, :method => :get, :class => "form-search" do + .input-append + = text_field_tag :query, params[:query], :id => 'user-typeahead', :class => "search-query", :autocomplete => :off + %button.btn{:type => :submit}= t(:search) + +%table.table.table-striped + %tr + %th= t(:username) + %th= t(:created) + %th= t(:updated) + = render @users.all diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml new file mode 100644 index 0000000..aecf831 --- /dev/null +++ b/app/views/users/new.html.haml @@ -0,0 +1,19 @@ +-# +-# This form is handled entirely by javascript, so take care when changing element ids. +-# + +- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} + +.span1 +.span9 + = render :partial => 'warnings' + %h2=t :signup + = simple_form_for(@user, form_options) do |f| + %legend= t(:signup_message) + = 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, :validate => true, :input_html => { :id => :srp_password_confirmation } + .form-actions + = f.button :submit, :value => t(:signup), :class => 'btn btn-primary' + = link_to t(:cancel), home_path, :class => 'btn' + diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml new file mode 100644 index 0000000..c587017 --- /dev/null +++ b/app/views/users/show.html.haml @@ -0,0 +1,25 @@ +.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[:billing] + .container-fluid + .row-fluid + = home_page_buttons(true) \ No newline at end of file diff --git a/app/views/v1/sessions/new.json.erb b/app/views/v1/sessions/new.json.erb new file mode 100644 index 0000000..36154b8 --- /dev/null +++ b/app/views/v1/sessions/new.json.erb @@ -0,0 +1,3 @@ +{ +"errors": <%= raw @errors.to_json %> +} diff --git a/app/views/webfinger/host_meta.xml.erb b/app/views/webfinger/host_meta.xml.erb new file mode 100644 index 0000000..cfcbcc0 --- /dev/null +++ b/app/views/webfinger/host_meta.xml.erb @@ -0,0 +1,11 @@ + + + + <%= @host_meta.subject %> + + <%- @host_meta.links.each do |rel, link| %> + + <%- end %> + diff --git a/app/views/webfinger/search.xml.erb b/app/views/webfinger/search.xml.erb new file mode 100644 index 0000000..7328552 --- /dev/null +++ b/app/views/webfinger/search.xml.erb @@ -0,0 +1,7 @@ + + + <%= @presenter.subject %> + <%- @presenter.links.each do |rel, link| %> + type=<%=link[:type]%> href="<%= link[:key] %>"/> + <% end %> + -- cgit v1.2.3 From c1486cb9688d53c5ae266ff22ab279ead12eaa36 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Apr 2014 12:45:21 +0200 Subject: move certs into toplevel cleaned up all the engine stuff that was never really used. Afterwards there is not that much left that makes it into the toplevel. --- app/controllers/v1/certs_controller.rb | 50 +++++++++++++++ app/models/client_certificate.rb | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 app/controllers/v1/certs_controller.rb create mode 100644 app/models/client_certificate.rb (limited to 'app') diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb new file mode 100644 index 0000000..64cfa7f --- /dev/null +++ b/app/controllers/v1/certs_controller.rb @@ -0,0 +1,50 @@ +class V1::CertsController < ApplicationController + + before_filter :require_login, :unless => :anonymous_certs_allowed? + + # GET /cert + def show + @cert = ClientCertificate.new(:prefix => certificate_prefix) + render text: @cert.to_s, content_type: 'text/plain' + end + + protected + + def anonymous_certs_allowed? + APP_CONFIG[:allow_anonymous_certs] + end + # + # this is some temporary logic until we store the service level in the user db. + # + # better logic might look like this: + # + # if logged_in? + # service_level = user.service_level + # elsif allow_anonymous? + # service_level = service_levels[:anonymous] + # else + # service_level = nil + # end + # + # if service_level.bandwidth == 'limited' && allow_limited? + # prefix = limited + # elsif allow_unlimited? + # prefix = unlimited + # else + # prefix = nil + # end + # + def certificate_prefix + if logged_in? + if APP_CONFIG[:allow_unlimited_certs] + APP_CONFIG[:unlimited_cert_prefix] + elsif APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:limited_cert_prefix] + end + elsif !APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:unlimited_cert_prefix] + else + APP_CONFIG[:limited_cert_prefix] + end + end +end diff --git a/app/models/client_certificate.rb b/app/models/client_certificate.rb new file mode 100644 index 0000000..76b07a2 --- /dev/null +++ b/app/models/client_certificate.rb @@ -0,0 +1,113 @@ +# +# Model for certificates +# +# This file must be loaded after Config has been loaded. +# +require 'base64' +require 'digest/md5' +require 'openssl' +require 'certificate_authority' +require 'date' + +class ClientCertificate + + attr_accessor :key # the client private RSA key + attr_accessor :cert # the client x509 certificate, signed by the CA + + # + # generate the private key and client certificate + # + def initialize(options = {}) + cert = CertificateAuthority::Certificate.new + + # set subject + cert.subject.common_name = common_name(options[:prefix]) + + # set expiration + cert.not_before = yesterday + cert.not_after = months_from_yesterday(APP_CONFIG[:client_cert_lifespan]) + + # generate key + cert.serial_number.number = cert_serial_number + cert.key_material.generate_key(APP_CONFIG[:client_cert_bit_size]) + + # sign + cert.parent = ClientCertificate.root_ca + cert.sign! client_signing_profile + + self.key = cert.key_material.private_key + self.cert = cert + end + + def to_s + self.key.to_pem + self.cert.to_pem + end + + private + + def self.root_ca + @root_ca ||= begin + crt = File.read(APP_CONFIG[:client_ca_cert]) + key = File.read(APP_CONFIG[:client_ca_key]) + openssl_cert = OpenSSL::X509::Certificate.new(crt) + cert = CertificateAuthority::Certificate.from_openssl(openssl_cert) + cert.key_material.private_key = OpenSSL::PKey::RSA.new(key, APP_CONFIG[:ca_key_password]) + cert + end + end + + # + # For cert serial numbers, we need a non-colliding number less than 160 bits. + # md5 will do nicely, since there is no need for a secure hash, just a short one. + # (md5 is 128 bits) + # + def cert_serial_number + Digest::MD5.hexdigest("#{rand(10**10)} -- #{Time.now}").to_i(16) + end + + def common_name(prefix = nil) + [prefix, random_common_name].join + end + + # + # for the random common name, we need a text string that will be unique across all certs. + # ruby 1.8 doesn't have a built-in uuid generator, or we would use SecureRandom.uuid + # + def random_common_name + cert_serial_number.to_s(36) + end + + def client_signing_profile + { + "digest" => APP_CONFIG[:client_cert_hash], + "extensions" => { + "keyUsage" => { + "usage" => ["digitalSignature"] + }, + "extendedKeyUsage" => { + "usage" => ["clientAuth"] + } + } + } + end + + ## + ## TIME HELPERS + ## + ## note: we use 'yesterday' instead of 'today', because times are in UTC, and some people on the planet + ## are behind UTC. + ## + + def yesterday + t = Time.now - 24*60*60 + Time.utc t.year, t.month, t.day + end + + def months_from_yesterday(num) + t = yesterday + date = Date.new t.year, t.month, t.day + date = date >> num # >> is months in the future operator + Time.utc date.year, date.month, date.day + end + +end -- cgit v1.2.3 From c6a22158c5bfb18fcd83434f92c55436fb15af23 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 10 Apr 2014 17:22:22 +0200 Subject: bringing back srp js --- app/assets/javascripts/application.js | 1 - app/assets/javascripts/leap_web_users/.gitkeep | 0 app/assets/javascripts/srp | 1 + 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 app/assets/javascripts/leap_web_users/.gitkeep create mode 160000 app/assets/javascripts/srp (limited to 'app') diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 03a40da..ab07e1f 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -20,4 +20,3 @@ //= require platform //= require tickets //= require users -//= require_tree . diff --git a/app/assets/javascripts/leap_web_users/.gitkeep b/app/assets/javascripts/leap_web_users/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/assets/javascripts/srp b/app/assets/javascripts/srp new file mode 160000 index 0000000..8f33d32 --- /dev/null +++ b/app/assets/javascripts/srp @@ -0,0 +1 @@ +Subproject commit 8f33d32d40b1e21ae7fb9a92c78a275422af4217 -- cgit v1.2.3 From 8cc5ba134f6c5a1a06d91407aa78b962545c54ac Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 11:42:13 +0200 Subject: initial commit for the service level api :api/service will return a hash of the current users service level This is failiing if the user is not logged in. Instead it should return the service description for an anonymous user. --- app/controllers/v1/services_controller.rb | 8 ++++++++ app/models/service_level.rb | 1 + 2 files changed, 9 insertions(+) create mode 100644 app/controllers/v1/services_controller.rb (limited to 'app') diff --git a/app/controllers/v1/services_controller.rb b/app/controllers/v1/services_controller.rb new file mode 100644 index 0000000..594940e --- /dev/null +++ b/app/controllers/v1/services_controller.rb @@ -0,0 +1,8 @@ +class V1::ServicesController < ApplicationController + + respond_to :json + + def show + respond_with current_user.effective_service_level + end +end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 299aaf1..31a713b 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -16,4 +16,5 @@ class ServiceLevel APP_CONFIG[:service_levels][@id] end + delegate :to_json, to: :config_hash end -- cgit v1.2.3 From 614745c84cab37dd03f2bd8f06160fd01c7fabdb Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 12:06:38 +0200 Subject: UnauthenticatedUser as current_user this still allows us to do current_user.service_level. Have not gone through the rest of the code yet. Only made sure logged_in? now tests for is_a? User instead of !!current_user --- app/controllers/controller_extension/authentication.rb | 12 +++++++++--- app/models/unauthenticated_user.rb | 7 +++++++ 2 files changed, 16 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 03d3989..2bc0aee 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -8,11 +8,11 @@ module ControllerExtension::Authentication end def current_user - @current_user ||= token_authenticate || warden.user + @current_user ||= token_authenticate || warden.user || unauthenticated end def logged_in? - !!current_user + current_user.is_a? User end def require_login @@ -42,7 +42,7 @@ module ControllerExtension::Authentication end def admin? - current_user && current_user.is_admin? + current_user.is_admin? end def require_admin @@ -72,4 +72,10 @@ module ControllerExtension::Authentication request.env['warden.options'] && request.env['warden.options'][:attempted_path] end + + protected + + def unauthenticated + UnauthenticatedUser.new + end end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb index 0fc17d2..ba6470a 100644 --- a/app/models/unauthenticated_user.rb +++ b/app/models/unauthenticated_user.rb @@ -3,4 +3,11 @@ 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. + def is_admin? + false + end + + def effective_service_level + ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + end end -- cgit v1.2.3 From 7a9ece43bd61246b450471ed6bb1089570321e38 Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 17 Apr 2014 19:27:47 +0200 Subject: make use of the UnauthorizedUser Null Pattern for current_user - use it to get rid of some conditionals --- app/controllers/v1/certs_controller.rb | 44 +++++-------------------------- app/controllers/v1/messages_controller.rb | 5 ++-- app/models/service_level.rb | 14 +++++++++- app/models/unauthenticated_user.rb | 20 +++++++++++--- 4 files changed, 39 insertions(+), 44 deletions(-) (limited to 'app') diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index 64cfa7f..580c90c 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,50 +1,20 @@ class V1::CertsController < ApplicationController - before_filter :require_login, :unless => :anonymous_certs_allowed? + before_filter :require_eip_access # GET /cert def show - @cert = ClientCertificate.new(:prefix => certificate_prefix) + @cert = ClientCertificate.new(:prefix => service_level.cert_prefix) render text: @cert.to_s, content_type: 'text/plain' end protected - def anonymous_certs_allowed? - APP_CONFIG[:allow_anonymous_certs] + def require_eip_access + access_denied unless service_level.provides?(:eip) end - # - # this is some temporary logic until we store the service level in the user db. - # - # better logic might look like this: - # - # if logged_in? - # service_level = user.service_level - # elsif allow_anonymous? - # service_level = service_levels[:anonymous] - # else - # service_level = nil - # end - # - # if service_level.bandwidth == 'limited' && allow_limited? - # prefix = limited - # elsif allow_unlimited? - # prefix = unlimited - # else - # prefix = nil - # end - # - def certificate_prefix - if logged_in? - if APP_CONFIG[:allow_unlimited_certs] - APP_CONFIG[:unlimited_cert_prefix] - elsif APP_CONFIG[:allow_limited_certs] - APP_CONFIG[:limited_cert_prefix] - end - elsif !APP_CONFIG[:allow_limited_certs] - APP_CONFIG[:unlimited_cert_prefix] - else - APP_CONFIG[:limited_cert_prefix] - end + + def service_level + current_user.effective_service_level end end diff --git a/app/controllers/v1/messages_controller.rb b/app/controllers/v1/messages_controller.rb index f71d0f1..85156b7 100644 --- a/app/controllers/v1/messages_controller.rb +++ b/app/controllers/v1/messages_controller.rb @@ -7,12 +7,11 @@ module V1 respond_to :json def index - render json: (current_user ? current_user.messages : [] ) + render json: current_user.messages end def update - message = Message.find(params[:id]) - if (message and current_user) + if message = Message.find(params[:id]) message.mark_as_read_by(current_user) message.save render json: true diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 31a713b..d0bd9b3 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -13,8 +13,20 @@ class ServiceLevel end def config_hash - APP_CONFIG[:service_levels][@id] + @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access end delegate :to_json, to: :config_hash + + def provides?(service) + services.include? service.to_s + end + + def services + config_hash[:services] || [] + end + + def cert_prefix + config_hash[:cert_prefix] + end end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb index ba6470a..7845a6f 100644 --- a/app/models/unauthenticated_user.rb +++ b/app/models/unauthenticated_user.rb @@ -1,13 +1,27 @@ # 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. + def effective_service_level + ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + end def is_admin? false end - def effective_service_level - ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] + def id + nil + end + + def email_address + nil + end + + def login + nil + end + + def messages + [] end end -- cgit v1.2.3 From 9216ab8252246a263c5d17f6755a7d3887145f94 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 11:55:40 +0200 Subject: change service level configuration strategy The changes to the configuration required some non minor changes to the platform and also added some flexibility we don't require yet - and thus some new possibilities for errors. So instead we still use the allow_..._certs and ..._cert_prefix options. They basically provide the framework in which service levels can operate. The service level configuration will not include the cert prefix anymore. It only states if the service level is rate limited or not. This avoids conflicts between the two configuration options. I also removed the anonymous service level entirely. It was also turning a boolean decision (do we provide anonymous eip or not) into something way more complex. Instead I added the AnonymousServiceLevel class to handle the corner cases for people who are not logged in. Furthermore i renamed the UnauthenticatedUser to AnonymousUser so it matches the Anonymous Service Level nicely. It's also shorter and more intuitive. --- .../controller_extension/authentication.rb | 6 ++--- app/controllers/v1/certs_controller.rb | 6 ++--- app/models/anonymous_service_level.rb | 31 ++++++++++++++++++++++ app/models/anonymous_user.rb | 27 +++++++++++++++++++ app/models/service_level.rb | 30 ++++++++++++--------- app/models/unauthenticated_user.rb | 27 ------------------- app/views/users/_change_service_level.html.haml | 4 +-- 7 files changed, 84 insertions(+), 47 deletions(-) create mode 100644 app/models/anonymous_service_level.rb create mode 100644 app/models/anonymous_user.rb delete mode 100644 app/models/unauthenticated_user.rb (limited to 'app') diff --git a/app/controllers/controller_extension/authentication.rb b/app/controllers/controller_extension/authentication.rb index 2bc0aee..1f73f38 100644 --- a/app/controllers/controller_extension/authentication.rb +++ b/app/controllers/controller_extension/authentication.rb @@ -8,7 +8,7 @@ module ControllerExtension::Authentication end def current_user - @current_user ||= token_authenticate || warden.user || unauthenticated + @current_user ||= token_authenticate || warden.user || anonymous end def logged_in? @@ -75,7 +75,7 @@ module ControllerExtension::Authentication protected - def unauthenticated - UnauthenticatedUser.new + def anonymous + AnonymousUser.new end end diff --git a/app/controllers/v1/certs_controller.rb b/app/controllers/v1/certs_controller.rb index 580c90c..73409ef 100644 --- a/app/controllers/v1/certs_controller.rb +++ b/app/controllers/v1/certs_controller.rb @@ -1,6 +1,6 @@ class V1::CertsController < ApplicationController - before_filter :require_eip_access + before_filter :require_login, :unless => :anonymous_certs_allowed? # GET /cert def show @@ -10,8 +10,8 @@ class V1::CertsController < ApplicationController protected - def require_eip_access - access_denied unless service_level.provides?(:eip) + def anonymous_certs_allowed? + APP_CONFIG[:allow_anonymous_certs] end def service_level diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb new file mode 100644 index 0000000..c51ce9e --- /dev/null +++ b/app/models/anonymous_service_level.rb @@ -0,0 +1,31 @@ +class AnonymousServiceLevel + + delegate :to_json, to: :config_hash + + def cert_prefix + if APP_CONFIG[:allow_limited_certs] + APP_CONFIG[:limited_cert_prefix] + else + APP_CONFIG[:unlimited_cert_prefix] + end + end + + def description + if APP_CONFIG[:allow_anonymous_certs] + "anonymous access to the VPN" + else + "please login to access our services" + end + end + + protected + + def config_hash + { name: "anonymous", + description: description, + cost: 0, + eip_rate_limit: APP_CONFIG[:allow_limited_certs] + } + end + +end diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb new file mode 100644 index 0000000..360a577 --- /dev/null +++ b/app/models/anonymous_user.rb @@ -0,0 +1,27 @@ +# The nil object for the user class +class AnonymousUser < Object + + def effective_service_level + AnonymousServiceLevel.new + end + + def is_admin? + false + end + + def id + nil + end + + def email_address + nil + end + + def login + nil + end + + def messages + [] + end +end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index d0bd9b3..06ad202 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -4,29 +4,35 @@ class ServiceLevel @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 + def self.select_options + APP_CONFIG[:service_levels].map do |id,config_hash| + [config_hash[:description], id] + end end def id @id end - def config_hash - @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access - end - delegate :to_json, to: :config_hash - def provides?(service) - services.include? service.to_s + def cert_prefix + if limited_cert? + APP_CONFIG[:limited_cert_prefix] + else + APP_CONFIG[:unlimited_cert_prefix] + end end - def services - config_hash[:services] || [] + protected + + def limited_cert? + APP_CONFIG[:allow_limited_certs] && + (!APP_CONFIG[:allow_unlimited_certs] || config_hash[:eip_rate_limit]) end - def cert_prefix - config_hash[:cert_prefix] + def config_hash + @config_hash || APP_CONFIG[:service_levels][@id].with_indifferent_access end + end diff --git a/app/models/unauthenticated_user.rb b/app/models/unauthenticated_user.rb deleted file mode 100644 index 7845a6f..0000000 --- a/app/models/unauthenticated_user.rb +++ /dev/null @@ -1,27 +0,0 @@ -# The nil object for the user class -class UnauthenticatedUser < Object - - def effective_service_level - ServiceLevel.new id: APP_CONFIG[:unauthenticated_service_level] - end - - def is_admin? - false - end - - def id - nil - end - - def email_address - nil - end - - def login - nil - end - - def messages - [] - end -end diff --git a/app/views/users/_change_service_level.html.haml b/app/views/users/_change_service_level.html.haml index 61e67d9..42315a2 100644 --- a/app/views/users/_change_service_level.html.haml +++ b/app/views/users/_change_service_level.html.haml @@ -8,11 +8,11 @@ %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 + = f.select :desired_service_level_code, ServiceLevel.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 + = f.select :effective_service_level_code, ServiceLevel.select_options, :selected => @user.effective_service_level.id .control-group .controls = f.submit t(:save), :class => 'btn', :data => {"loading-text" => "Saving..."} -- cgit v1.2.3 From 966e390d401b84dad98127e647d2ec634f1cbc15 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 12:39:27 +0200 Subject: bringing back empty cert prefixes if neither limited nor unlimited certs are allowed there will be no prefix. Not sure if this is desired - but it's the way things used to be before the refactoring --- app/models/anonymous_service_level.rb | 2 +- app/models/service_level.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb index c51ce9e..47b7cfb 100644 --- a/app/models/anonymous_service_level.rb +++ b/app/models/anonymous_service_level.rb @@ -5,7 +5,7 @@ class AnonymousServiceLevel def cert_prefix if APP_CONFIG[:allow_limited_certs] APP_CONFIG[:limited_cert_prefix] - else + elsif APP_CONFIG[:allow_unlimited_certs] APP_CONFIG[:unlimited_cert_prefix] end end diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 06ad202..5dd8838 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -19,7 +19,7 @@ class ServiceLevel def cert_prefix if limited_cert? APP_CONFIG[:limited_cert_prefix] - else + elsif APP_CONFIG[:allow_unlimited_certs] APP_CONFIG[:unlimited_cert_prefix] end end -- cgit v1.2.3 From be81b7430e0a2046125be7c3a4b01b8725f4afe6 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 18 Apr 2014 12:51:18 +0200 Subject: adopt service_level config to platform settings cost -> rate quota -> storage --- app/models/anonymous_service_level.rb | 1 - 1 file changed, 1 deletion(-) (limited to 'app') diff --git a/app/models/anonymous_service_level.rb b/app/models/anonymous_service_level.rb index 47b7cfb..4366a4a 100644 --- a/app/models/anonymous_service_level.rb +++ b/app/models/anonymous_service_level.rb @@ -23,7 +23,6 @@ class AnonymousServiceLevel def config_hash { name: "anonymous", description: description, - cost: 0, eip_rate_limit: APP_CONFIG[:allow_limited_certs] } end -- cgit v1.2.3 From dad200e2cb0b03995eb06024d3fcf788ad2486ff Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 10:24:24 +0200 Subject: add signup and login info on the forms --- app/views/sessions/new.html.haml | 1 + app/views/users/new.html.haml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 771dc97..316eec1 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -2,6 +2,7 @@ .span9 = render :partial => 'users/warnings' %h2=t :login + .lead=t :login_info = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } = f.input :password, :required => false, :input_html => { :id => :srp_password } diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index aecf831..173dd8c 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -8,8 +8,8 @@ .span9 = render :partial => 'warnings' %h2=t :signup + .lead=t :signup_info = simple_form_for(@user, form_options) do |f| - %legend= t(:signup_message) = 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, :validate => true, :input_html => { :id => :srp_password_confirmation } -- cgit v1.2.3 From 1da12752c3b3aa3d4a03e67ede133e807e9434df Mon Sep 17 00:00:00 2001 From: Azul Date: Thu, 24 Apr 2014 11:10:01 +0200 Subject: cleanup homepage buttons some let's devide the partials rather than having super specific parameters (on_user_page) --- app/helpers/core_helper.rb | 4 ++-- app/views/common/_action_buttons.html.haml | 11 +++++++++++ app/views/common/_download_button.html.haml | 6 ++++++ app/views/common/_home_page_buttons.html.haml | 27 ++++++--------------------- app/views/home/_content.html.haml | 2 -- app/views/users/show.html.haml | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 app/views/common/_action_buttons.html.haml create mode 100644 app/views/common/_download_button.html.haml (limited to 'app') diff --git a/app/helpers/core_helper.rb b/app/helpers/core_helper.rb index a6c7479..4126906 100644 --- a/app/helpers/core_helper.rb +++ b/app/helpers/core_helper.rb @@ -6,8 +6,8 @@ module CoreHelper # # insert common buttons (download, login, etc) # - def home_page_buttons(on_user_page = false) - render 'common/home_page_buttons', {:on_user_page => on_user_page} + def home_page_buttons + render 'common/home_page_buttons' end end diff --git a/app/views/common/_action_buttons.html.haml b/app/views/common/_action_buttons.html.haml new file mode 100644 index 0000000..c74fcd1 --- /dev/null +++ b/app/views/common/_action_buttons.html.haml @@ -0,0 +1,11 @@ +.home-buttons + .row-fluid.second + .login.span4 + %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') + %span.info= t(:login_info) + .signup.span4 + %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') + %span.info= t(:signup_info) + .help.span4 + %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') + %span.info= t(:help_info) diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml new file mode 100644 index 0000000..157c40a --- /dev/null +++ b/app/views/common/_download_button.html.haml @@ -0,0 +1,6 @@ +.home-buttons + .row-fluid.first + .span2 + .download.span8 + = render partial: 'common/download_for_os', collection: available_clients + ['other'] + .span2 diff --git a/app/views/common/_home_page_buttons.html.haml b/app/views/common/_home_page_buttons.html.haml index c9ea7a2..8c47983 100644 --- a/app/views/common/_home_page_buttons.html.haml +++ b/app/views/common/_home_page_buttons.html.haml @@ -1,23 +1,8 @@ - icon_color = :black -.home-buttons - .row-fluid.first - .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] - - if !local_assigns[:on_user_page] - .row-fluid.second - .login.span4 - %span.link= link_to(icon('ok-sign', icon_color) + t(:login), login_path, :class => 'btn') - %span.info= t(:login_info) - .signup.span4 - %span.link= link_to(icon('user', icon_color) + t(:signup), signup_path, :class => 'btn') - %span.info= t(:signup_info) - .help.span4 - %span.link= link_to(icon('question-sign', icon_color) + t(:get_help), new_ticket_path, :class => 'btn') - %span.info= t(:help_info) += render 'common/download_button' +- if local_assigns[:divider] + .row-fluid + .span12 + = render local_assigns[:divider] += render 'common/action_buttons', icon_color: icon_color diff --git a/app/views/home/_content.html.haml b/app/views/home/_content.html.haml index 3d351e9..e47fdaf 100644 --- a/app/views/home/_content.html.haml +++ b/app/views/home/_content.html.haml @@ -8,7 +8,5 @@ - if Rails.env == 'development' .row-fluid %hr - %p - = link_to "fetch a cert", cert_path %p = link_to "make donation", new_payment_path if APP_CONFIG[:payment].present? diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index c587017..3c73147 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -22,4 +22,4 @@ %li= icon('shopping-cart') + link_to(t(:overview_billing), billing_top_link(@user)) if APP_CONFIG[:billing] .container-fluid .row-fluid - = home_page_buttons(true) \ No newline at end of file + = render 'common/download_button' -- cgit v1.2.3 From 689c10b618c6c469ce82a7f09e117567def577de Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 14:58:30 +0200 Subject: simple form: add wrapped and loading... buttons #5542 the loading... text on the buttons was not capitalized before. So in order to change this in a (more or less) single place i added new button types to simple_form: button :wrapped - normal button, with loading and an optional cancel button wrapped in the classical bootstrap action div. cancel option contains the url to go to when canceling. button :loading - simple button with loading text capitalized by using i18n (simple_form.buttons.loading) Conflicts: engines/support/app/views/tickets/new.html.haml --- app/views/sessions/new.html.haml | 4 +--- app/views/users/new.html.haml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 316eec1..6f3b324 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -6,6 +6,4 @@ = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } = f.input :password, :required => false, :input_html => { :id => :srp_password } - .form-actions - = f.button :submit, :value => t(:login), :class => 'btn-primary' - = link_to t(:cancel), home_path, :class => 'btn' + = f.button :wrapped, value: t(:login), cancel: home_path diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index 173dd8c..3478989 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -13,7 +13,5 @@ = 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, :validate => true, :input_html => { :id => :srp_password_confirmation } - .form-actions - = f.button :submit, :value => t(:signup), :class => 'btn btn-primary' - = link_to t(:cancel), home_path, :class => 'btn' + = f.button :wrapped, value: t(:signup), cancel: home_path -- cgit v1.2.3 From e727d211630591a3aaed5eba0eb50c7d4964b6f8 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 23 Apr 2014 16:54:40 +0200 Subject: ensure buttons are properly loading... and reset --- app/assets/javascripts/application.js | 1 + app/assets/javascripts/buttons.js | 39 +++++++++++++++++++++++++++++++++++ app/assets/javascripts/users.js | 21 +++---------------- 3 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 app/assets/javascripts/buttons.js (limited to 'app') diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ab07e1f..9af373d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -20,3 +20,4 @@ //= require platform //= require tickets //= require users +//= require buttons diff --git a/app/assets/javascripts/buttons.js b/app/assets/javascripts/buttons.js new file mode 100644 index 0000000..7142957 --- /dev/null +++ b/app/assets/javascripts/buttons.js @@ -0,0 +1,39 @@ +/* + * Buttons.js + * + * Use bootstrap loading state after submitting a form. + * + * Some form inputs are validaded before the submission + * so triggering loading state on click events is not a + * good idea. If the validation fails the errors will + * be displayed but the button would be in loading state. + * + * We used to trigger it based on form submission but + * we have a few forms that contain multiple buttons. + * So now we mark the buttons as clicked on click and + * put the clicked button into loading state on submit. + * + */ + +(function() { + markAsClicked = function () { + var btn = $(this) + btn.addClass('clicked') + setTimeout(function () { + btn.removeClass('clicked') + }, 1000) + }; + + markAsLoading = function(submitEvent) { + var form = submitEvent.target; + $(form).addClass('submitted') + // bootstrap loading state: + $(form).find('.btn.clicked[type="submit"]').button('loading'); + }; + + $(document).ready(function() { + $('form').submit(markAsLoading); + $('.btn[type="submit"]').click(markAsClicked); + }); + +}).call(this); diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 8486756..4a86ef5 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -35,24 +35,10 @@ data: $(form).serialize() }); req.done( function() { - $(form).find('input[type="submit"]').button('reset'); + $(form).find('.btn[type="submit"]').button('reset'); }); }; - - markAsSubmitted = function(submitEvent) { - var form = submitEvent.target; - $(form).addClass('submitted') - // bootstrap loading state: - $(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 // @@ -79,7 +65,7 @@ clear_errors(); var errors = extractErrors(message); displayErrors(errors); - resetButtons(); + $('.btn[type="submit"]').button('reset'); } function extractErrors(message) { @@ -115,7 +101,6 @@ // $(document).ready(function() { - $('form').submit(markAsSubmitted); $('#new_user').submit(prevent_default); $('#new_user').submit(srp.signup); $('#new_session').submit(prevent_default); -- cgit v1.2.3 From a2909d781a790e47acbdbb8b4560177100ad9942 Mon Sep 17 00:00:00 2001 From: Azul Date: Fri, 25 Apr 2014 16:51:03 +0200 Subject: basic password validation without client side gem The client_side_validations gem is not maintained anymore and the validations were not working lately. So instead of trying to fix it I started working on independent validations for the password as it can't be validated on the server due to SRP. So far these validations are very primitive. They require 8 characters length and a matching confirmation. --- app/assets/javascripts/users.js | 80 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index 4a86ef5..fa20399 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -1,13 +1,24 @@ (function() { + + + var settings = { + "error_class":"help-inline", + "error_tag":"span", + "wrapper_error_class":"error", + "wrapper_tag":"div", + "wrapper_class":"control-group" + } + // // LOCAL FUNCTIONS // - var poll_users, - prevent_default, - form_failed, - form_passed, + var poll_users, + prevent_default, clear_errors, + clear_field_errors, + validate_password_confirmation, + validate_password_length, update_user; prevent_default = function(event) { @@ -24,6 +35,10 @@ return $('#messages').empty(); }; + clear_field_errors = function() { + return $(settings.error_tag + '.' + settings.error_class).remove(); + }; + update_user = function(submitEvent) { var form = submitEvent.target; var token = form.dataset.token; @@ -38,7 +53,34 @@ $(form).find('.btn[type="submit"]').button('reset'); }); }; - + + validate_password_confirmation = function(submitEvent) { + var form = submitEvent.target; + var password = $(form).find('input#srp_password').val(); + var confirmation = $(form).find('input#srp_password_confirmation').val(); + if (password === confirmation) { + return true; + } + else { + displayFieldError("password_confirmation", "does not match."); + submitEvent.stopImmediatePropagation() + return false; + } + }; + + validate_password_length = function(submitEvent) { + var form = submitEvent.target; + var password = $(form).find('input#srp_password').val(); + if (password.length > 7) { + return true; + } + else { + displayFieldError("password", "needs to be at least 8 characters long."); + submitEvent.stopImmediatePropagation() + return false; + } + }; + // // PUBLIC FUNCTIONS // @@ -90,18 +132,38 @@ } function displayFieldError(field, error) { + var message = $.isArray(error) ? error[0] : error; var element = $('form input[name$="[' + field + ']"]'); if (element) { - element.trigger('element:validate:fail.ClientSideValidations', error).data('valid', false); + addError(element, settings, message); } }; - // - // INIT - // + function addError(element, settings, message) { + var errorElement, wrapper; + + errorElement = element.parent().find("" + settings.error_tag + "." + settings.error_class); + wrapper = element.closest(settings.wrapper_tag + '.' + settings.wrapper_class); + if (errorElement[0] == null) { + errorElement = $("<" + settings.error_tag + "/>", { + "class": settings.error_class, + text: message + }); + element.after(errorElement); + } + wrapper.addClass(settings.wrapper_error_class); + return errorElement.text(message); + } + +// +// INIT +// $(document).ready(function() { $('#new_user').submit(prevent_default); + $('#new_user').submit(clear_field_errors); + $('#new_user').submit(validate_password_length); + $('#new_user').submit(validate_password_confirmation); $('#new_user').submit(srp.signup); $('#new_session').submit(prevent_default); $('#new_session').submit(srp.login); -- cgit v1.2.3 From c75268538a63692a371fd03a999721f252af2a98 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:23:22 +0200 Subject: simplify download button --- app/assets/stylesheets/leap.scss | 2 +- app/views/common/_download_button.html.haml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 4c0dfe3..2a0c100 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -231,13 +231,13 @@ input, textarea { .download { a.btn { width: 14em; + font-weight: bold; small { font-weight: normal; } } } a.btn { - font-weight: bold; width: 11em; margin: 10px auto; display: block; diff --git a/app/views/common/_download_button.html.haml b/app/views/common/_download_button.html.haml index 157c40a..e57c56b 100644 --- a/app/views/common/_download_button.html.haml +++ b/app/views/common/_download_button.html.haml @@ -2,5 +2,7 @@ .row-fluid.first .span2 .download.span8 - = render partial: 'common/download_for_os', collection: available_clients + ['other'] + = link_to client_download_url, class: "btn btn-large btn-primary" do + = big_icon('download') + = t(:download_client) .span2 -- cgit v1.2.3 From 8219db0c388f8b8caf7293cbd60086a1aeef8eb4 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:31:24 +0200 Subject: smaller download button with context on dash To use bitmask services: Download Bitmask --- app/views/users/show.html.haml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3c73147..ddc33ab 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -20,6 +20,11 @@ - # %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[:billing] + + .container-fluid .row-fluid - = render 'common/download_button' + %h4 To use bitmask services: + = link_to client_download_url, class: "btn btn-primary" do + %i.icon-arrow-down.icon-white + = t(:download_client) -- cgit v1.2.3 From 5b3d92e40000e14c913c52227b5a717201cc5074 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 12:34:32 +0200 Subject: remove os specific download buttons keeping the platform detection for now. --- app/assets/stylesheets/leap.scss | 59 ----------------------------- app/views/common/_download_for_os.html.haml | 17 --------- 2 files changed, 76 deletions(-) delete mode 100644 app/views/common/_download_for_os.html.haml (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index 2a0c100..d4db879 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -43,65 +43,6 @@ display: none; } -// -// OS specific -// - -.os-android { - display: none !important; -} - -html.android .os-android { - display: inherit !important; -} - -.os-linux { - display: none !important; -} - -html.linux .os-linux { - 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 // diff --git a/app/views/common/_download_for_os.html.haml b/app/views/common/_download_for_os.html.haml deleted file mode 100644 index 3a11d10..0000000 --- a/app/views/common/_download_for_os.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- 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 - = big_icon('download') - .pull-right - = 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 -- cgit v1.2.3 From 229b93f58787d86a41cba2a894f04d5c351d7a56 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 29 Apr 2014 13:56:14 +0200 Subject: adopt pricing view to current service_level format --- app/views/pages/pricing.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/views/pages/pricing.html.haml b/app/views/pages/pricing.html.haml index 77699d8..ce4b379 100644 --- a/app/views/pages/pricing.html.haml +++ b/app/views/pages/pricing.html.haml @@ -13,9 +13,9 @@ %td= level[:name] %td= level[:description] %td - - if level[:cost].nil? || level[:cost] == 0 + - if level[:rate].nil? || level[:rate] == 0 = t(:free) - else - = level[:cost].map{|currency,cost| "#{cost} #{currency}"}.join(', ') + = level[:rate].join(', ') - else - No service levels are configured. \ No newline at end of file + No service levels are configured. -- cgit v1.2.3 From 3952a7dfcd13993bd51ac7dc07cb3a01b4658e25 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 30 Apr 2014 16:35:39 +0200 Subject: hide srp forms when no js is available Hiding them using two mechanisms in case one fails: .hidden class - bootstrap hides them then style='display:none' - so they are hidden even if css load fails --- app/assets/javascripts/users.js | 2 ++ app/helpers/users_helper.rb | 2 +- app/views/sessions/new.html.haml | 4 ++-- app/views/users/_warnings.html.haml | 2 +- app/views/users/new.html.haml | 8 +++++--- 5 files changed, 11 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js index fa20399..e6c2fcc 100644 --- a/app/assets/javascripts/users.js +++ b/app/assets/javascripts/users.js @@ -160,6 +160,8 @@ // $(document).ready(function() { + $('.hidden.js-show').removeClass('hidden'); + $('.js-show').show(); $('#new_user').submit(prevent_default); $('#new_user').submit(clear_field_errors); $('#new_user').submit(validate_password_length); diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index f56faab..1b2dc5d 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,7 +1,7 @@ module UsersHelper def user_form_class(*classes) - (classes + ['user', 'form', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') + (classes + ['user', 'hidden', 'js-show', (@user.new_record? ? 'new' : 'edit')]).compact.join(' ') end def wrapped(item, options = {}) diff --git a/app/views/sessions/new.html.haml b/app/views/sessions/new.html.haml index 6f3b324..bb7e4bd 100644 --- a/app/views/sessions/new.html.haml +++ b/app/views/sessions/new.html.haml @@ -1,9 +1,9 @@ .span1 .span9 - = render :partial => 'users/warnings' %h2=t :login .lead=t :login_info - = simple_form_for [:api, @session], :validate => true, :html => { :id => :new_session, :class => 'form-horizontal' } do |f| + = render :partial => 'users/warnings' + = simple_form_for [:api, @session], validate: true, html: { id: :new_session, class: 'form-horizontal hidden js-show', style: "display:none;" } do |f| = f.input :login, :required => false, :label => t(:username), :input_html => { :id => :srp_username } = f.input :password, :required => false, :input_html => { :id => :srp_password } = f.button :wrapped, value: t(:login), cancel: home_path diff --git a/app/views/users/_warnings.html.haml b/app/views/users/_warnings.html.haml index 79ab103..baf80a4 100644 --- a/app/views/users/_warnings.html.haml +++ b/app/views/users/_warnings.html.haml @@ -9,4 +9,4 @@ document.getElementById('cookie_warning').style.display = 'block'; } else { document.getElementById('cookie_warning').style.display = 'none'; - } \ No newline at end of file + } diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index 3478989..bf7ae31 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -1,14 +1,16 @@ -# --# This form is handled entirely by javascript, so take care when changing element ids. +-# This form is handled entirely by javascript +-# Please take care when changing element ids. +-# The form hidden when no js is available to prevent submission in the clear. -# -- form_options = {:url => '/not-used', :html => {:id => 'new_user', :class => user_form_class('form-horizontal')}, :validate => true} +- form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} .span1 .span9 - = render :partial => 'warnings' %h2=t :signup .lead=t :signup_info + = render :partial => 'warnings' = simple_form_for(@user, form_options) do |f| = 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 } -- cgit v1.2.3 From 616313b406e561b45a73e34f54fd5fc7595f9658 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 6 May 2014 09:34:38 +0200 Subject: minor: fixing comment --- app/views/users/new.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/users/new.html.haml b/app/views/users/new.html.haml index bf7ae31..bc36068 100644 --- a/app/views/users/new.html.haml +++ b/app/views/users/new.html.haml @@ -1,7 +1,9 @@ -# -# This form is handled entirely by javascript -# Please take care when changing element ids. --# The form hidden when no js is available to prevent submission in the clear. +-# +-# The form is hidden when no js is available +-# to prevent submission in the clear. -# - form_options = {url: '/not-used', html: {id: 'new_user', class: user_form_class('form-horizontal'), style: 'display:none'}, validate: true} -- cgit v1.2.3 From cf6aa0a6c8852424e39e4785a1bd783ab475376b Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 8 May 2014 15:12:15 -0700 Subject: minor fix to default pricing chart --- app/views/pages/pricing.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/pages/pricing.html.haml b/app/views/pages/pricing.html.haml index ce4b379..e339d27 100644 --- a/app/views/pages/pricing.html.haml +++ b/app/views/pages/pricing.html.haml @@ -16,6 +16,6 @@ - if level[:rate].nil? || level[:rate] == 0 = t(:free) - else - = level[:rate].join(', ') + = level[:rate].collect{|currency, price| "#{currency} #{price}"}.join(', ') - else No service levels are configured. -- cgit v1.2.3 From 86eb9062f1e81302647bf18ce0f5fd981202b68a Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 09:51:36 +0200 Subject: allow for usernames with dots preparing for #5664 with some test improvements i ran into this issue This commit includes a fix and the test improvements. In particular it adds BrowserIntegrationTest#login - so there is no need to go through the signup procedure everytime you want a user to be logged in. --- app/controllers/v1/sessions_controller.rb | 2 +- app/models/identity.rb | 6 ++++++ app/models/token.rb | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/v1/sessions_controller.rb b/app/controllers/v1/sessions_controller.rb index eae3a1e..d88fcdc 100644 --- a/app/controllers/v1/sessions_controller.rb +++ b/app/controllers/v1/sessions_controller.rb @@ -38,7 +38,7 @@ module V1 def login_response handshake = session.delete(:handshake) || {} - handshake.to_hash.merge(:id => current_user.id, :token => @token.id) + handshake.to_hash.merge(:id => current_user.id, :token => @token.to_s) end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 9b97b51..ad8c01e 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -70,6 +70,12 @@ class Identity < CouchRest::Model::Base end end + def self.destroy_all_for(user) + Identity.by_user_id.key(user.id).each do |identity| + identity.destroy + end + end + def self.destroy_all_disabled Identity.disabled.each do |identity| identity.destroy diff --git a/app/models/token.rb b/app/models/token.rb index 4856c31..e759ee3 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -30,6 +30,10 @@ class Token < CouchRest::Model::Base end end + def to_s + id + end + def authenticate if expired? destroy -- cgit v1.2.3 From 0261e82686ec4fcfc8b633664fadb1dd6d9c8070 Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 10:52:55 +0200 Subject: keep empty email field if user removed prefill We should respect the users choice. We can still get their email from the user id if we really need to. --- app/models/service_level.rb | 8 ++++++++ app/models/user.rb | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/service_level.rb b/app/models/service_level.rb index 5dd8838..a8df55b 100644 --- a/app/models/service_level.rb +++ b/app/models/service_level.rb @@ -24,6 +24,14 @@ class ServiceLevel end end + def provides?(service) + services.include? service + end + + def services + config_hash[:services] || [] + end + protected def limited_cert? diff --git a/app/models/user.rb b/app/models/user.rb index c297ac8..a809879 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -62,7 +62,9 @@ class User < CouchRest::Model::Base end def email_address - LocalEmail.new(login) + if effective_service_level.provides?('email') + LocalEmail.new(login) + end end # Since we are storing admins by login, we cannot allow admins to change their login. -- cgit v1.2.3 From bbe9de73352b5aa937173b4158267f6a37e9ca5f Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 14:03:53 +0200 Subject: destinguish user.email from user.email_address use the former if you want a working email account or nil, the latter if you want the email address associated with a given user no matter if the user actually has an email account or not. --- app/models/anonymous_user.rb | 4 ++++ app/models/user.rb | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb index 360a577..87239eb 100644 --- a/app/models/anonymous_user.rb +++ b/app/models/anonymous_user.rb @@ -13,6 +13,10 @@ class AnonymousUser < Object nil end + def email + nil + end + def email_address nil end diff --git a/app/models/user.rb b/app/models/user.rb index a809879..6678de6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -61,12 +61,19 @@ class User < CouchRest::Model::Base login end - def email_address + # use this if you want to get a working email address only. + def email if effective_service_level.provides?('email') - LocalEmail.new(login) + email_address end end + # use this if you want the email address associated with a + # user no matter if the user actually has a local email account + def email_address + LocalEmail.new(login) + end + # Since we are storing admins by login, we cannot allow admins to change their login. def is_admin? APP_CONFIG['admins'].include? self.login -- cgit v1.2.3 From 503fa755ee7f5ec52e833a05fb640b1c16e89a5f Mon Sep 17 00:00:00 2001 From: Azul Date: Tue, 13 May 2014 14:45:49 +0200 Subject: move User Control Panel heading out of masthead the masthead can only handle content of a limited width. Alternatively we could make the masthead title wider and add padding on the left so it still aligns nicely with the mask. However the wider we make it the worse it looks on small width displays. Another option would be to make the masthead contain multiple lines. However vertical aligment to the middle of 1 OR 2 lines of text is a real pain. So I went with a super simple masthead and the Caption goes below. --- app/views/layouts/_header.html.haml | 2 +- app/views/layouts/_masthead.html.haml | 2 -- app/views/layouts/application.html.haml | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/views/layouts/_header.html.haml b/app/views/layouts/_header.html.haml index 157f1df..a1dd47a 100644 --- a/app/views/layouts/_header.html.haml +++ b/app/views/layouts/_header.html.haml @@ -8,5 +8,5 @@ %li = link_to t(:logout), logout_path, :method => :delete - if @user && @show_navigation - .user_heading + .lead = @user.email_address diff --git a/app/views/layouts/_masthead.html.haml b/app/views/layouts/_masthead.html.haml index 35225a1..fde5915 100644 --- a/app/views/layouts/_masthead.html.haml +++ b/app/views/layouts/_masthead.html.haml @@ -2,5 +2,3 @@ .title %span.sitename %a{:href => home_path}= APP_CONFIG[:domain] - - if @show_navigation - = t(:user_control_panel) \ No newline at end of file diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 1cd4ec3..d213fe1 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -14,6 +14,9 @@ = render 'layouts/masthead' #main .container-fluid + - if @show_navigation + .row-fluid + %h1= t(:user_control_panel) - if logged_in? .row-fluid .span12 -- cgit v1.2.3 From eb74538841c6cf687b87f5373b90a22847e974fa Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 14 May 2014 16:46:38 +0200 Subject: make download button wide enough for helvetica #5039 --- app/assets/stylesheets/leap.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/leap.scss b/app/assets/stylesheets/leap.scss index d4db879..77104e5 100644 --- a/app/assets/stylesheets/leap.scss +++ b/app/assets/stylesheets/leap.scss @@ -171,7 +171,7 @@ input, textarea { } .download { a.btn { - width: 14em; + width: 15em; font-weight: bold; small { font-weight: normal; -- cgit v1.2.3 From 702e444e57d7904e429d1628e600d11e2e793a62 Mon Sep 17 00:00:00 2001 From: Azul Date: Wed, 14 May 2014 19:47:26 +0200 Subject: check for presence of service_levels This way the pricing link will not be shown for an empty hash. Which is easier to get into the config file than nil. And we can later verify the configuration to have a hash as the service_levels. --- app/views/layouts/_footer.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/layouts/_footer.html.haml b/app/views/layouts/_footer.html.haml index 5909bdd..340d36c 100644 --- a/app/views/layouts/_footer.html.haml +++ b/app/views/layouts/_footer.html.haml @@ -6,5 +6,5 @@ = link_to icon('info-sign') + t(:about), about_path - if lookup_context.exists?('pages/contact') = link_to icon('comment') + t(:contact), contact_path - - if APP_CONFIG[:service_levels] - = link_to icon('shopping-cart') + t(:pricing), pricing_path \ No newline at end of file + - if APP_CONFIG[:service_levels].present? + = link_to icon('shopping-cart') + t(:pricing), pricing_path -- cgit v1.2.3