diff --git a/Gemfile b/Gemfile index c72bd8326..9296044c0 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ group :test do end require_relative "lib/bootstrap" -unless Bootstrap.local_authentication? +unless Bootstrap.oss_config? eval_gemfile "gems/fizzy-saas/Gemfile" gem "fizzy-saas", path: "gems/fizzy-saas" end diff --git a/app/assets/images/collection.svg b/app/assets/images/collection.svg index 5d6249839..cdbe7d4da 100644 --- a/app/assets/images/collection.svg +++ b/app/assets/images/collection.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/stylesheets/card-columns.css b/app/assets/stylesheets/card-columns.css index f4ff6ac05..1a50f110f 100644 --- a/app/assets/stylesheets/card-columns.css +++ b/app/assets/stylesheets/card-columns.css @@ -73,10 +73,6 @@ z-index: 3; } - &:not(.cards--considering, .is-collapsed, .cards--grid) { - overflow: clip; - } - &.is-collapsed { border-radius: calc(var(--column-width-collapsed) / 2); flex: 0 0 var(--column-width-collapsed); diff --git a/app/assets/stylesheets/events.css b/app/assets/stylesheets/events.css index 28d6b7c80..42d129125 100644 --- a/app/assets/stylesheets/events.css +++ b/app/assets/stylesheets/events.css @@ -97,6 +97,17 @@ position: absolute; } + .events__filter-select { + font-weight: inherit; + text-decoration: underline; + + @media (any-hover: hover) { + &:hover { + --btn-color: var(--color-link); + } + } + } + .events__day { position: relative; diff --git a/app/assets/stylesheets/rich-text-content.css b/app/assets/stylesheets/rich-text-content.css index 18641da1a..873ad8cd9 100644 --- a/app/assets/stylesheets/rich-text-content.css +++ b/app/assets/stylesheets/rich-text-content.css @@ -248,7 +248,7 @@ } .attachment--file { - --attachment-icon-color: var(--lexxy-color-text-subtle); + --attachment-icon-color: var(--color-ink-medium); align-items: center; display: flex; @@ -266,6 +266,15 @@ color: var(--color-ink); font-weight: bold; } + + /* Video attachments don't have an identifiable class, but we need to + * make sure the caption is always below the video */ + &:has(video) { + .attachment__caption { + flex: none; + inline-size: 100%; + } + } } .attachment--psd, diff --git a/app/controllers/concerns/authentication.rb b/app/controllers/concerns/authentication.rb index 89871a9f5..2ede3c108 100644 --- a/app/controllers/concerns/authentication.rb +++ b/app/controllers/concerns/authentication.rb @@ -37,7 +37,9 @@ module Authentication end def require_tenant - ApplicationRecord.current_tenant.present? || request_authentication + unless ApplicationRecord.current_tenant.present? + render "sessions/login_menu" + end end def require_authentication diff --git a/app/helpers/accesses_helper.rb b/app/helpers/accesses_helper.rb index 8e3dee6a3..c446dc5a2 100644 --- a/app/helpers/accesses_helper.rb +++ b/app/helpers/accesses_helper.rb @@ -50,7 +50,7 @@ module AccessesHelper collection_involvement_path(collection), method: :put, aria: { labelledby: involvement_label_id }, - class: "btn", + class: class_names("btn", { "btn--reversed": access.involvement == "watching" && icon_only }), params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement), icon_only: icon_only } ) do safe_join([ diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index ee665e98a..6c67400c7 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -54,8 +54,8 @@ module PaginationHelper def day_timeline_pagination_link(day_timeline, filter) if day_timeline.next_day - link_to "Load more...", events_days_path(day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params), - data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } + link_to "Load more…", events_days_path(day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params), + class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } end end diff --git a/app/javascript/controllers/collapsible_columns_controller.js b/app/javascript/controllers/collapsible_columns_controller.js index 84ef0d315..5002c46e2 100644 --- a/app/javascript/controllers/collapsible_columns_controller.js +++ b/app/javascript/controllers/collapsible_columns_controller.js @@ -14,7 +14,7 @@ export default class extends Controller { } preventToggle(event) { - if (event.detail.attributeName === "class") { + if (event.target.hasAttribute("data-collapsible-columns-target") && event.detail.attributeName === "class") { event.preventDefault() } } diff --git a/app/javascript/controllers/filter_settings_controller.js b/app/javascript/controllers/filter_settings_controller.js index 7a207da80..6be549e5c 100644 --- a/app/javascript/controllers/filter_settings_controller.js +++ b/app/javascript/controllers/filter_settings_controller.js @@ -5,7 +5,7 @@ import { post } from "@rails/request.js" export default class extends Controller { static classes = ["filtersSet"] static targets = ["field", "form"] - static values = { refreshUrl: String, noFilteringUrl: String } + static values = { refreshUrl: String, noFilteringUrl: String, cardsUrl: String } initialize() { this.debouncedToggle = debounce(this.#toggle.bind(this), 50) @@ -31,6 +31,12 @@ export default class extends Controller { this.debouncedToggle() } + submitToGenericCardsView() { + this.formTarget.action = this.cardsUrlValue + this.formTarget.dataset.turboFrame = "top" + this.formTarget.requestSubmit() + } + #toggle() { this.element.classList.toggle(this.filtersSetClass, this.#hasFiltersSet) } diff --git a/app/javascript/controllers/multi_selection_combobox_controller.js b/app/javascript/controllers/multi_selection_combobox_controller.js index 70a853b2b..80d7f0826 100644 --- a/app/javascript/controllers/multi_selection_combobox_controller.js +++ b/app/javascript/controllers/multi_selection_combobox_controller.js @@ -82,15 +82,14 @@ export default class extends Controller { #clearHiddenFields() { this.element.querySelectorAll('input[type="hidden"]').forEach(field => { - if (field !== this.hiddenField) { - field.remove() - } + field.remove() }) } #addHiddenFields() { this.#selectedValues().forEach(value => { - const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children + const [ field ] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children + field.removeAttribute("id") field.value = value this.element.appendChild(field) }) diff --git a/app/models/account.rb b/app/models/account.rb index 950c0a124..c29f61005 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -4,7 +4,7 @@ class Account < ApplicationRecord has_many_attached :uploads def slug - "/#{tenant_id}" + "/#{tenant}" end def setup_basic_template diff --git a/app/models/card.rb b/app/models/card.rb index 9175d6b96..c837c781a 100644 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -22,7 +22,6 @@ class Card < ApplicationRecord case index when "stalled" then stalled when "postponing_soon" then postponing_soon - when "falling_back_soon" then falling_back_soon when "closed" then closed.recently_closed_first when "golden" then golden when "draft" then drafted diff --git a/app/models/filter/fields.rb b/app/models/filter/fields.rb index fc11752fc..027ae8ac6 100644 --- a/app/models/filter/fields.rb +++ b/app/models/filter/fields.rb @@ -1,7 +1,7 @@ module Filter::Fields extend ActiveSupport::Concern - INDEXES = %w[ all stalled postponing_soon falling_back_soon golden draft ] + INDEXES = %w[ all stalled postponing_soon golden draft ] SORTED_BY = %w[ newest oldest latest ] delegate :default_value?, to: :class diff --git a/app/models/user/filtering.rb b/app/models/user/filtering.rb index 9f321bbed..6e00ecbc1 100644 --- a/app/models/user/filtering.rb +++ b/app/models/user/filtering.rb @@ -67,6 +67,10 @@ class User::Filtering filter.closers.any? end + def show_collections? + filter.collections.any? + end + def enable_collection_filtering(&block) @collection_filtering_route_resolver = block end diff --git a/app/views/cards/display/_preview.html.erb b/app/views/cards/display/_preview.html.erb index 3f3b4b139..ce8f3d026 100644 --- a/app/views/cards/display/_preview.html.erb +++ b/app/views/cards/display/_preview.html.erb @@ -27,7 +27,7 @@ diff --git a/app/views/events/event/_layout.html.erb b/app/views/events/event/_layout.html.erb index 63f8e696d..69a1560e6 100644 --- a/app/views/events/event/_layout.html.erb +++ b/app/views/events/event/_layout.html.erb @@ -1,9 +1,11 @@ <%= link_to event.notifiable_target, class: "event event--#{ event.action } #{ "golden-effect" if event.card.golden? } center-block flex flex-column full-width align-start justify-start position-relative", - style: "--card-color: #{ card.closed? ? "var(--color-card-complete)" : card.color };", target: "_top", - data: { related_element_target: "related", - related_element_group_value: card.id, - action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %> + style: "--card-color: #{ card.closed? ? "var(--color-card-complete)" : card.color };", + target: "_top", + data: { + related_element_target: "related", + related_element_group_value: card.id, + action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>

<%= card.id %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index dad76f7f2..8acc92214 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -7,10 +7,9 @@ <%= render "filters/menu", user_filtering: @user_filtering %> <%= render "events/index/add_card_button", user_filtering: @user_filtering %> -

-
- Activity in <%= @user_filtering.selected_collections_label %> -
+

+ Activity in + <%= render "events/index/filter", user_filtering: @user_filtering %>

<% end %> diff --git a/app/views/events/index/_filter.html.erb b/app/views/events/index/_filter.html.erb new file mode 100644 index 000000000..f48ee4a6c --- /dev/null +++ b/app/views/events/index/_filter.html.erb @@ -0,0 +1,43 @@ +<% filter = user_filtering.filter %> + +<%= form_with url: events_path, + method: :get, class: "flex-inline position-relative", + data: { controller: "form", turbo_frame: "cards_container", turbo_action: "advance" } do |form| %> + <%= tag.div class: "flex-inline position-relative quick-filter", + data: { + controller: "dialog multi-selection-combobox", + action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", + filter_show: user_filtering.show_collections?, + multi_selection_combobox_no_selection_label_value: user_filtering.selected_collections_label } do %> + + + + + <%= filter_dialog "Collection…" do %> + Collection… + + <% if user_filtering.collections.many? %> + <%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true, + type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> + <% end %> + + + <% end %> + <% end %> +<% end %> diff --git a/app/views/filters/_settings.html.erb b/app/views/filters/_settings.html.erb index 5642197fb..b8f907c45 100644 --- a/app/views/filters/_settings.html.erb +++ b/app/views/filters/_settings.html.erb @@ -6,8 +6,9 @@ toggle_class_toggle_class: "filters--expanded", filter_settings_filters_set_class: "filters--has-filters-set", filter_settings_no_filtering_url_value: no_filtering_url, - turbo_permanent: true, - filter_settings_refresh_url_value: settings_refresh_path } do %> + filter_settings_refresh_url_value: settings_refresh_path, + filter_settings_cards_url_value: cards_path, + turbo_permanent: true } do %> <%= form_with url: filter_url, method: :get, class: "", data: { controller: "form", turbo_frame: "cards_container", @@ -15,9 +16,6 @@ action: "turbo:submit-end->filter-settings#resetIfNoFiltering", turbo_action: "advance" } do |form| %> <%= hidden_field_tag :expand_all, true, disabled: !user_filtering.expanded?, data: { toggle_enable_target: "element"} %> - <% user_filtering.filter.collections.each do |collection| %> - <%= filter_hidden_field_tag "collection_ids[]", collection.id %> - <% end %> <%= yield form if block_given? %> diff --git a/app/views/filters/menu/_shortcut_notice.html.erb b/app/views/filters/menu/_shortcut_notice.html.erb index b594b19a0..70f5f5fb7 100644 --- a/app/views/filters/menu/_shortcut_notice.html.erb +++ b/app/views/filters/menu/_shortcut_notice.html.erb @@ -1,5 +1,5 @@ <% if platform.desktop? %> <% end %> diff --git a/app/views/filters/menu/collections/_all_option.html.erb b/app/views/filters/menu/collections/_all_option.html.erb index 4c933510f..8ee95a795 100644 --- a/app/views/filters/menu/collections/_all_option.html.erb +++ b/app/views/filters/menu/collections/_all_option.html.erb @@ -2,15 +2,8 @@ <% if Current.user.collections.many? %> diff --git a/app/views/filters/menu/collections/_collection.html.erb b/app/views/filters/menu/collections/_collection.html.erb index 48a8ac223..b4c1e1cc3 100644 --- a/app/views/filters/menu/collections/_collection.html.erb +++ b/app/views/filters/menu/collections/_collection.html.erb @@ -1,17 +1,5 @@ diff --git a/app/views/filters/settings/_collections.html.erb b/app/views/filters/settings/_collections.html.erb new file mode 100644 index 000000000..c1bf2d9c2 --- /dev/null +++ b/app/views/filters/settings/_collections.html.erb @@ -0,0 +1,40 @@ +<% filter = user_filtering.filter %> + +<%= tag.div class: "quick-filter position-relative", + data: { + controller: "dialog multi-selection-combobox", + action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside", + filter_show: user_filtering.show_collections?, + multi_selection_combobox_no_selection_label_value: "Collection…", + multi_selection_combobox_label_prefix_value: "" } do %> + + + + + <%= filter_dialog "Collection…" do %> + Collection… + + <% if user_filtering.collections.many? %> + <%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true, + type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> + <% end %> + + + <% end %> +<% end %> diff --git a/app/views/filters/settings/_controls.html.erb b/app/views/filters/settings/_controls.html.erb index 60f3338e3..65fd101ad 100644 --- a/app/views/filters/settings/_controls.html.erb +++ b/app/views/filters/settings/_controls.html.erb @@ -1,3 +1,4 @@ +<%= render "filters/settings/collections", user_filtering: user_filtering %> <%= render "filters/settings/sorted_by", user_filtering: user_filtering %> <%= render "filters/settings/indexed_by", user_filtering: user_filtering %> <%= render "filters/settings/tags", user_filtering: user_filtering %> diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb index 0922c2de4..25b06cae9 100644 --- a/app/views/layouts/shared/_head.html.erb +++ b/app/views/layouts/shared/_head.html.erb @@ -19,7 +19,9 @@ <%= tenanted_action_cable_meta_tag %> <%= yield :head %> - + <% if ApplicationRecord.current_tenant %> + + <% end %> diff --git a/app/views/sessions/login_menu.html.erb b/app/views/sessions/login_menu.html.erb new file mode 100644 index 000000000..e8c78fc61 --- /dev/null +++ b/app/views/sessions/login_menu.html.erb @@ -0,0 +1,15 @@ +<% cache ApplicationRecord.tenants do %> +

We're migrating Fizzy authentication

+

+ While we migrate Fizzy away from Launchpad and 37id, please login at one of the following URLs: + +

+

+

+ Mike should have sent you your temporary password by now. If you haven't received it, please ping him! +

+<% end %> diff --git a/app/views/users/_activity_timeline.html.erb b/app/views/users/_activity_timeline.html.erb index 1c3b84d56..cbe5ce302 100644 --- a/app/views/users/_activity_timeline.html.erb +++ b/app/views/users/_activity_timeline.html.erb @@ -3,8 +3,8 @@ <%= render "events/day", day_timeline: day_timeline %> <% if day_timeline.next_day %> - <%= link_to "Load more...", user_path(user, day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params), - data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } %> + <%= link_to "Load more…", user_path(user, day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params), + class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } %> <% end %> <% end %>
diff --git a/bin/dev b/bin/dev index 9fe70d369..1c77800e3 100755 --- a/bin/dev +++ b/bin/dev @@ -1,12 +1,10 @@ #!/usr/bin/env sh -echo "Login with david@37signals.com / secret123456 to:" bin/rails runner - < "accounts#new" resources :accounts, only: %i[ new create ] diff --git a/gems/fizzy-saas/lib/fizzy/saas/engine.rb b/gems/fizzy-saas/lib/fizzy/saas/engine.rb index 2729e1db3..4e5e93489 100644 --- a/gems/fizzy-saas/lib/fizzy/saas/engine.rb +++ b/gems/fizzy-saas/lib/fizzy/saas/engine.rb @@ -5,10 +5,7 @@ module Fizzy class Engine < ::Rails::Engine # extend application models config.to_prepare do - User.prepend User::SignalUser Account.prepend Account::SignalAccount - LoginHelper.prepend LoginHelper::SignalLogin - SessionsController.include Sessions::SignalSessions end # moved from config/initializers/queenbee.rb @@ -39,7 +36,6 @@ module Fizzy silence_warnings do SignalId::Account::Peer = Account - SignalId::User::Peer = User end end diff --git a/gems/fizzy-saas/test/controllers/controller_authentication_test.rb b/gems/fizzy-saas/test/controllers/controller_authentication_test.rb deleted file mode 100644 index bf1fc21aa..000000000 --- a/gems/fizzy-saas/test/controllers/controller_authentication_test.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "test_helper" - -class ControllerAuthenticationTest < ActionDispatch::IntegrationTest - test "access without an account slug redirects to launchpad" do - integration_session.default_url_options[:script_name] = "" # no tenant - - get cards_path - - assert_redirected_to Launchpad.login_url(product: true) - end - - test "access with an account slug but no session redirects to launchpad" do - get cards_path - - assert_redirected_to Launchpad.login_url(product: true, account: Account.sole) - end -end diff --git a/gems/fizzy-saas/test/controllers/sessions/launchpad_controller_test.rb b/gems/fizzy-saas/test/controllers/sessions/launchpad_controller_test.rb deleted file mode 100644 index 309f6ec1a..000000000 --- a/gems/fizzy-saas/test/controllers/sessions/launchpad_controller_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -require "test_helper" - -class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest - test "show renders when not signed in" do - get saas.session_launchpad_path(params: { sig: "test-sig" }) - - assert_response :success - - assert_select "form input#sig" do |node| - assert_equal node.length, 1 - assert_equal node.first["value"], "test-sig" - end - end - - test "create establishes a session when the sig is valid" do - user = users(:david) - - put saas.session_launchpad_path(params: { sig: user.external_user.perishable_signature }) - - assert_redirected_to root_url - assert parsed_cookies.signed[:session_token] - end - - test "create checks user.active?" do - user = users(:david) - user.update! active: false - - put saas.session_launchpad_path(params: { sig: user.external_user.perishable_signature }) - - assert_response :unauthorized - end - - test "returns 401 when the sig is invalid" do - put saas.session_launchpad_path(params: { sig: "invalid" }) - - assert_response :unauthorized - end -end diff --git a/gems/fizzy-saas/test/controllers/sessions_controller_test.rb b/gems/fizzy-saas/test/controllers/sessions_controller_test.rb deleted file mode 100644 index 98df25bdf..000000000 --- a/gems/fizzy-saas/test/controllers/sessions_controller_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "test_helper" - -class SessionsControllerTest < ActionDispatch::IntegrationTest - test "destroy" do - sign_in_as :kevin - - delete session_path - - assert_redirected_to Launchpad.logout_url - assert_not cookies[:session_token].present? - end - - test "new" do - get new_session_path - - assert_response :forbidden - end - - test "create" do - post session_path, params: { email_address: "david@37signals.com", password: "secret123456" } - - assert_response :forbidden - end -end diff --git a/gems/fizzy-saas/test/models/account/signal_account_test.rb b/gems/fizzy-saas/test/models/account/signal_account_test.rb index 623f95610..ad43e45cb 100644 --- a/gems/fizzy-saas/test/models/account/signal_account_test.rb +++ b/gems/fizzy-saas/test/models/account/signal_account_test.rb @@ -18,7 +18,7 @@ class Account::SignalAccountTest < ActiveSupport::TestCase assert_equal @account, @account.external_account.peer end - test ".create_with_admin_user creates a new local account and user peers" do + test ".create_with_admin_user creates a new local account" do ApplicationRecord.create_tenant("account-create-with-dependents") do signal_account = signal_accounts(:honcho_fizzy) account = Account.create_with_admin_user(tenant_id: signal_account.queenbee_id) @@ -32,11 +32,9 @@ class Account::SignalAccountTest < ActiveSupport::TestCase assert_equal 1, User.count User.first.tap do |user| - assert signal_account.owner.name, user.name - assert signal_account.owner.email_address, user.email_address - assert signal_account.owner.id, user.external_user_id + assert_equal signal_account.owner.name, user.name + assert_equal signal_account.owner.email_address, user.email_address assert_equal "admin", user.role - assert_equal user, signal_account.owner.peer end end end diff --git a/gems/fizzy-saas/test/models/signup_test.rb b/gems/fizzy-saas/test/models/signup_test.rb index 96e432840..e64dbf801 100644 --- a/gems/fizzy-saas/test/models/signup_test.rb +++ b/gems/fizzy-saas/test/models/signup_test.rb @@ -42,7 +42,6 @@ class SignupTest < ActiveSupport::TestCase assert @signup.user assert @signup.user.persisted? - assert_equal @signup.user.external_user, @signup.signal_account.owner assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name assert_includes ApplicationRecord.tenants, @signup.tenant_name @@ -86,7 +85,6 @@ class SignupTest < ActiveSupport::TestCase assert @signup.user assert @signup.user.persisted? - assert_equal @signup.user.external_user, @signup.signal_account.owner assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name assert_includes ApplicationRecord.tenants, @signup.tenant_name diff --git a/gems/fizzy-saas/test/models/user/signal_user_test.rb b/gems/fizzy-saas/test/models/user/signal_user_test.rb deleted file mode 100644 index b244a587e..000000000 --- a/gems/fizzy-saas/test/models/user/signal_user_test.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "test_helper" - -class User::SignalUserTest < ActiveSupport::TestCase - setup do - @user = users(:david) - end - - test "belongs to a Signal::User" do - assert_not_nil @user.external_user_id - assert_equal signal_users("37s_fizzy_david"), @user.external_user - end - - test "peering" do - assert_equal @user, @user.external_user.peer - end - - test "deactivate clears signal user" do - users(:jz).deactivate - - assert_nil users(:jz).reload.external_user - end -end diff --git a/gems/fizzy-saas/test/test_helper.rb b/gems/fizzy-saas/test/test_helper.rb index 22d468a6c..648afa3ee 100644 --- a/gems/fizzy-saas/test/test_helper.rb +++ b/gems/fizzy-saas/test/test_helper.rb @@ -4,10 +4,6 @@ require "queenbee/testing/mocks" module ActiveSupport class TestCase include SignalId::Testing - - def saas_extension_sign_in_as(user) - put saas.session_launchpad_path, params: { sig: user.external_user.perishable_signature } - end end end diff --git a/lib/bootstrap.rb b/lib/bootstrap.rb index 1cfe4042c..6d52f3f49 100644 --- a/lib/bootstrap.rb +++ b/lib/bootstrap.rb @@ -1,5 +1,5 @@ module Bootstrap - def self.local_authentication? - ENV.fetch("LOCAL_AUTHENTICATION", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__)) + def self.oss_config? + ENV.fetch("OSS_CONFIG", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__)) end end diff --git a/script/create-local-user.rb b/script/create-local-user.rb deleted file mode 100755 index 7b303774f..000000000 --- a/script/create-local-user.rb +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env ruby -# -# This script creates a new user that can be logged into via LOCAL_AUTHENTICATION, avoiding Launchpad/37id integration. -# In order to login as this user, you must set the `LOCAL_AUTHENTICATION` environment variable when running the Rails server: -# -# LOCAL_AUTHENTICATION=1 bin/dev -# - -require_relative "../config/environment" - -unless Rails.env.development? - puts "ERROR: This script is intended to be run in development mode only." - exit 1 -end - -if ARGV.length < 2 - puts "Usage: #{$0} " - exit 1 -end - -email_address = ARGV[0] -tenant = ARGV[1] - -ApplicationRecord.with_tenant(tenant) do - user = User.create!( - name: email_address.split("@").first, - email_address: email_address, - password: "secret123456" - ) - - puts "Created: " - pp user -end diff --git a/script/create-new-user.rb b/script/create-new-user.rb index 934719447..ab30c585e 100755 --- a/script/create-new-user.rb +++ b/script/create-new-user.rb @@ -2,41 +2,25 @@ require_relative "../config/environment" -if ARGV.length < 2 - puts "Usage: #{$0} " +if ARGV.length < 3 + puts "Usage: #{$0} " exit 1 end -email_address = ARGV[0] -tenant = ARGV[1] +tenant = ARGV.shift +email_address = ARGV.shift +name = ARGV.join(" ") -def confirm(noun) - print "Is this the correct #{noun}? (y/n) " - response = $stdin.gets.chomp.downcase - exit 0 unless response == "y" - puts -end - -signal_identity = SignalId::Identity.find_by!(email_address: email_address) -pp signal_identity -confirm "identity" - -ApplicationRecord.with_tenant(tenant) do - signal_account = Account.sole.external_account - pp signal_account - confirm "account" - - SignalId::Database.on_master do - signal_user = SignalId::User.create!(identity: signal_identity, account: signal_account) - - user = User.create!( - name: signal_user.name, - email_address: signal_user.email_address, - external_user_id: signal_user.id, - password: SecureRandom.hex(36) # TODO: remove password column? - ) +begin + ApplicationRecord.with_tenant(tenant) do + password = SecureRandom.hex(16) + user = User.create!(name:, email_address:, password:) puts "Created: " - pp [ user, signal_user ] + pp user + + puts "Password is: #{password.inspect}" end +rescue Exception => e + puts "Failed with error: #{e.inspect}" end diff --git a/script/load-prod-db-in-dev.rb b/script/load-prod-db-in-dev.rb index a619b1f8d..da0db12ba 100755 --- a/script/load-prod-db-in-dev.rb +++ b/script/load-prod-db-in-dev.rb @@ -34,7 +34,6 @@ ActiveRecord::Tenanted::DatabaseTasks.migrate_all ApplicationRecord.with_tenant(signup.tenant_name) do |tenant| Account.sole.update! external_account: signup.signal_account - User.first.update! external_user: signup.signal_account.owner puts "\n\nLogin to http://launchpad.localhost:3011/ as #{signup.email_address} / #{signup.password}" end diff --git a/script/migrations/202510-migrate-to-local-authentication.rb b/script/migrations/202510-migrate-to-local-authentication.rb new file mode 100755 index 000000000..cd8788092 --- /dev/null +++ b/script/migrations/202510-migrate-to-local-authentication.rb @@ -0,0 +1,53 @@ +#!/usr/bin/env ruby + +# +# set up a temporary password for all users +# while we migrate away from Launchpad and 37id. +# +# the password is unguessable but generated based on the email address and a seed, +# so each user will have the same password across all tenants. +# +require_relative "../../config/environment" + +seed = ARGV[0] || "temporary-password-seed" +puts "Using seed: #{seed.inspect}" + +TENANTS = Hash.new +USERS = Hash.new { |h, k| h[k] = Hash.new } + +ApplicationRecord.with_each_tenant do |tenant| + TENANTS[tenant] = Account.sole.name + + User.find_each do |user| + putc "." + next if user.system? + + if user.external_user_id + suser = SignalId::User.find_by_id(user.external_user_id) + if suser && suser.email_address != user.email_address + puts "\nWarning: fixing email address for user #{user.id} in tenant #{tenant}:" + puts " local: #{user.email_address}" + puts " signal: #{suser.email_address}" + user.update! email_address: suser.email_address + end + end + + password = Digest::SHA256.hexdigest("#{seed}-#{user.email_address}")[0..16] + user.update! password: password + + USERS[user.email_address][tenant] = password + end +end + +puts + +USERS.each do |email, hash| + puts "\n#{email}:" + puts " password: #{hash.first.last}" + puts " fizzies:" + hash.each do |tenant, _| + url = Rails.application.routes.url_helpers.root_url(Rails.application.config.action_controller.default_url_options.merge(script_name: "/#{tenant}")) + + puts " #{TENANTS[tenant]}: #{url}" + end +end diff --git a/script/migrations/create-accounts-in-staging.rb b/script/migrations/create-accounts-in-staging.rb deleted file mode 100755 index d0279d608..000000000 --- a/script/migrations/create-accounts-in-staging.rb +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env ruby - -require_relative "../config/environment" - -ApplicationRecord.with_each_tenant do |tenant| - account = Account.sole - signal_account = account.signal_account - - signal_users = SignalId::User.where(account_id: signal_account.id) - - signal_users.each do |signal_user| - unless User.find_by(signal_user_id: signal_user.id) - User.create!( - name: signal_user.identity.name, - email_address: signal_user.identity.email_address, - signal_user_id: signal_user.id, - password: SecureRandom.hex(36) # TODO: remove password column? - ) - end - end -end diff --git a/script/stitch-signal-id-accounts.rb b/script/stitch-signal-id-accounts.rb deleted file mode 100755 index e08f594ae..000000000 --- a/script/stitch-signal-id-accounts.rb +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env ruby - -# -# this is intended to copy a production database into beta, change the account id, and stitch the -# user accounts together properly. -# - -require_relative "../config/environment" - -ActiveRecord::Base.logger = Logger.new(File::NULL) - -ApplicationRecord.with_each_tenant do |tenant| - puts "\n# tenant: #{tenant}" - - signal_account = SignalId::Account.find_by!(queenbee_id: tenant) - puts "Found signal account #{signal_account.inspect}" - - account = Account.sole - if account.tenant_id != tenant - puts "setting account tenant_id to #{tenant}" - account.update!(tenant_id: tenant, name: account.name + " (Beta)") - end - - User.find_each do |user| - next if user.system? || user.external_user_id.nil? - - signal_user = user.external_user - next if signal_user.nil? - next if signal_user.account == account.external_account - - signal_identity = signal_user.identity - pp signal_identity - - SignalId::Database.on_master do - signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account) - puts "Created signal user #{signal_user.inspect} for identity #{signal_identity.inspect}" - user.external_user_id = signal_user.id - user.save! - end - end -end diff --git a/test/controllers/controller_authentication_test.rb b/test/controllers/controller_authentication_test.rb index 1dfc7a3fd..72e0df995 100644 --- a/test/controllers/controller_authentication_test.rb +++ b/test/controllers/controller_authentication_test.rb @@ -1,20 +1,19 @@ require "test_helper" class ControllerAuthenticationTest < ActionDispatch::IntegrationTest - if Rails.application.config.x.local_authentication - test "access without an account slug redirects to new session" do - integration_session.default_url_options[:script_name] = "" # no tenant + test "access without an account slug redirects to new session" do + integration_session.default_url_options[:script_name] = "" # no tenant - get cards_path + get cards_path - assert_redirected_to new_session_path - end + assert_response :success + assert_dom "h2", text: "We're migrating Fizzy authentication" + end - test "access with an account slug but no session redirects to new session" do - get cards_path + test "access with an account slug but no session redirects to new session" do + get cards_path - assert_redirected_to new_session_path - end + assert_redirected_to new_session_path end test "access with an account slug and a session allows functional access" do diff --git a/test/controllers/sessions_controller_test.rb b/test/controllers/sessions_controller_test.rb index 9a8cf0caf..0c85ad83c 100644 --- a/test/controllers/sessions_controller_test.rb +++ b/test/controllers/sessions_controller_test.rb @@ -1,34 +1,32 @@ require "test_helper" class SessionsControllerTest < ActionDispatch::IntegrationTest - if Rails.application.config.x.local_authentication - test "destroy" do - sign_in_as :kevin + test "destroy" do + sign_in_as :kevin - delete session_path + delete session_path - assert_redirected_to new_session_path - assert_not cookies[:session_token].present? - end + assert_redirected_to new_session_path + assert_not cookies[:session_token].present? + end - test "new" do - get new_session_path + test "new" do + get new_session_path - assert_response :success - end + assert_response :success + end - test "create with valid credentials" do - post session_path, params: { email_address: "david@37signals.com", password: "secret123456" } + test "create with valid credentials" do + post session_path, params: { email_address: "david@37signals.com", password: "secret123456" } - assert_redirected_to root_path - assert cookies[:session_token].present? - end + assert_redirected_to root_path + assert cookies[:session_token].present? + end - test "create with invalid credentials" do - post session_path, params: { email_address: "david@37signals.com", password: "wrong" } + test "create with invalid credentials" do + post session_path, params: { email_address: "david@37signals.com", password: "wrong" } - assert_redirected_to new_session_path - assert_not cookies[:session_token].present? - end + assert_redirected_to new_session_path + assert_not cookies[:session_token].present? end end diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml index 73c15ea65..d12c1a4b3 100644 --- a/test/fixtures/users.yml +++ b/test/fixtures/users.yml @@ -5,27 +5,18 @@ david: email_address: david@37signals.com password_digest: <%= digest %> role: member -<% if User.reflect_on_association :external_user %> - external_user: 37s_fizzy_david -<% end %> jz: name: JZ email_address: jz@37signals.com password_digest: <%= digest %> role: member -<% if User.reflect_on_association :external_user %> - external_user: 37s_fizzy_jzimdars -<% end %> kevin: name: Kevin email_address: kevin@37signals.com password_digest: <%= digest %> role: admin -<% if User.reflect_on_association :external_user %> - external_user: 37s_fizzy_kevin -<% end %> system: name: System diff --git a/test/models/account_test.rb b/test/models/account_test.rb index dbede16f2..78554b997 100644 --- a/test/models/account_test.rb +++ b/test/models/account_test.rb @@ -3,6 +3,6 @@ require "test_helper" class AccountTest < ActiveSupport::TestCase test "slug" do account = Account.sole - assert_equal "/#{account.tenant_id}", account.slug + assert_equal "/#{ApplicationRecord.current_tenant}", account.slug end end diff --git a/test/models/period_highlights_test.rb b/test/models/period_highlights_test.rb index cf7627201..5bcfca99a 100644 --- a/test/models/period_highlights_test.rb +++ b/test/models/period_highlights_test.rb @@ -1,30 +1,27 @@ require "test_helper" class PeriodHighlightTest < ActiveSupport::TestCase - # Skipping when locally authenticating because the VCR cassettes would need to be re-recorded. - unless Rails.application.config.x.local_authentication - include VcrTestHelper + include VcrTestHelper - setup do - @user = users(:david) + setup do + @user = users(:david) + end + + test "generate period highlights" do + period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do + PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) end - test "generate period highlights" do - period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do - PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) - end + assert_match /logo/i, period_highlights.to_html + end - assert_match /logo/i, period_highlights.to_html + test "don't generate highlights for existing periods" do + new_period_highlights = PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) + + existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do + PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) end - test "don't generate highlights for existing periods" do - new_period_highlights = PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) - - existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do - PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months) - end - - assert_equal new_period_highlights, existing_period_highlights - end + assert_equal new_period_highlights, existing_period_highlights end end diff --git a/test/models/user/highlights_test.rb b/test/models/user/highlights_test.rb index 0344d02b8..7123f3bc2 100644 --- a/test/models/user/highlights_test.rb +++ b/test/models/user/highlights_test.rb @@ -1,67 +1,64 @@ require "test_helper" class User::HighlightsTest < ActiveSupport::TestCase - # Skipping when locally authenticating because the VCR cassettes would need to be re-recorded. - unless Rails.application.config.x.local_authentication - include VcrTestHelper + include VcrTestHelper - setup do - @user = users(:david) - travel_to 1.week.ago + 2.days - Current.session = sessions(:david) - end + setup do + @user = users(:david) + travel_to 1.week.ago + 2.days + Current.session = sessions(:david) + end - test "generate weekly highlights" do - stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do - period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do - @user.generate_weekly_highlights - end - - assert_match /logo/i, period_highlights.to_html - end - end - - test "don't generate highlights for existing periods" do - stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do - new_period_highlights = @user.generate_weekly_highlights - assert_not_nil new_period_highlights - - existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do - @user.generate_weekly_highlights - end - - assert_equal new_period_highlights, existing_period_highlights - end - end - - test "periods respect user timezone for week boundaries" do - @user.settings.update!(timezone_name: "America/New_York") - - # Sunday Jan 7, 2024 at 2am EST (7am UTC) - this is Sunday in NYC - sunday_in_nyc = Time.zone.parse("2024-01-07 07:00:00 UTC") - - # Saturday Jan 6, 2024 at 11pm EST (Jan 7 4am UTC) - still Saturday in NYC but Sunday in UTC - saturday_in_nyc = Time.zone.parse("2024-01-07 04:00:00 UTC") - - # Event on Saturday evening in NYC (but Sunday in UTC) - saturday_event = travel_to(saturday_in_nyc) { cards(:logo).track_event("Saturday event") } - - # Events throughout the week starting Sunday in NYC - 7.times do |i| - travel_to(sunday_in_nyc + i.days) { cards(:logo).track_event("Event #{i}") } + test "generate weekly highlights" do + stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do + period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do + @user.generate_weekly_highlights end - wednesday = sunday_in_nyc + 3.days - - # The period should start at Sunday in NYC timezone - period_start = wednesday.in_time_zone("America/New_York").beginning_of_week(:sunday) - period = PeriodHighlights::Period.new(@user.collections, starts_at: period_start, duration: 1.week) - - # The Saturday event should NOT be included (it's in the previous week in NYC time) - assert_not_includes period.events, saturday_event - - # Should include 7 events from the current week - assert_equal 7, period.events.count + assert_match /logo/i, period_highlights.to_html end end + + test "don't generate highlights for existing periods" do + stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do + new_period_highlights = @user.generate_weekly_highlights + assert_not_nil new_period_highlights + + existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do + @user.generate_weekly_highlights + end + + assert_equal new_period_highlights, existing_period_highlights + end + end + + test "periods respect user timezone for week boundaries" do + @user.settings.update!(timezone_name: "America/New_York") + + # Sunday Jan 7, 2024 at 2am EST (7am UTC) - this is Sunday in NYC + sunday_in_nyc = Time.zone.parse("2024-01-07 07:00:00 UTC") + + # Saturday Jan 6, 2024 at 11pm EST (Jan 7 4am UTC) - still Saturday in NYC but Sunday in UTC + saturday_in_nyc = Time.zone.parse("2024-01-07 04:00:00 UTC") + + # Event on Saturday evening in NYC (but Sunday in UTC) + saturday_event = travel_to(saturday_in_nyc) { cards(:logo).track_event("Saturday event") } + + # Events throughout the week starting Sunday in NYC + 7.times do |i| + travel_to(sunday_in_nyc + i.days) { cards(:logo).track_event("Event #{i}") } + end + + wednesday = sunday_in_nyc + 3.days + + # The period should start at Sunday in NYC timezone + period_start = wednesday.in_time_zone("America/New_York").beginning_of_week(:sunday) + period = PeriodHighlights::Period.new(@user.collections, starts_at: period_start, duration: 1.week) + + # The Saturday event should NOT be included (it's in the previous week in NYC time) + assert_not_includes period.events, saturday_event + + # Should include 7 events from the current week + assert_equal 7, period.events.count + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index a71063f10..fbb2b1149 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -65,6 +65,6 @@ RubyLLM.configure do |config| config.openai_api_key ||= "DUMMY-TEST-KEY" # Run tests with VCR without having to configure OpenAI API key locally. end -unless Rails.application.config.x.local_authentication +unless Rails.application.config.x.oss_config load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__) end diff --git a/test/test_helpers/session_test_helper.rb b/test/test_helpers/session_test_helper.rb index b04b06962..e857da327 100644 --- a/test/test_helpers/session_test_helper.rb +++ b/test/test_helpers/session_test_helper.rb @@ -7,11 +7,7 @@ module SessionTestHelper cookies.delete :session_token user = users(user) unless user.is_a? User - if Rails.application.config.x.local_authentication - post session_path, params: { email_address: user.email_address, password: "secret123456" } - else - saas_extension_sign_in_as(user) - end + post session_path, params: { email_address: user.email_address, password: "secret123456" } cookie = cookies.get_cookie "session_token" assert_not_nil cookie, "Expected session_token cookie to be set after sign in"