Merge branch 'main' into column-perma

* main: (47 commits)
  Remove overflow from columns
  Only reverse Watching button if icon_only
  Move video captions below content
  Account#slug uses the virtual "tenant" attribute
  Rename LOCAL_AUTHENTICATION to OSS_CONFIG
  Add a script to generate unique passwords per email address
  Visually hide the pagination indicator
  Use hide avatars from tabindex on preview
  Render a helpful menu of tenants temporarily
  Remove code related to Launchpad auth or 37id Users
  Formatting
  Fix missing close bracket from merge
  Reverse involvement buttons if watching
  Only render the manifest link for tenanted URLs
  Move load-balancer to fizzy-lb-101
  Add otel_collector accessory for load balancer
  Fix: this was getting idiomorph confused as it was being invoked on many subtree elements
  Keep column popups during page refreshes
  Don't render next link unless there is more activity
  Live updates for the collection perma
  ...
This commit is contained in:
Jason Zimdars
2025-10-02 17:36:56 -05:00
85 changed files with 493 additions and 610 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -359,7 +359,7 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.9.beta)
lexxy (0.1.10.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
+1 -1
View File
@@ -1 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m3 5h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8z"/></svg>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m21.8 0h-19.6c-1.2 0-2.2 1-2.2 2.2v19.5c0 1.2 1 2.2 2.2 2.2h19.5c1.2 0 2.2-1 2.2-2.2v-19.5c0-1.2-1-2.2-2.2-2.2zm-10.8 19h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8zm10 8h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8z"/></svg>

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 286 B

-4
View File
@@ -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);
+11
View File
@@ -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;
-6
View File
@@ -31,12 +31,6 @@
position: absolute;
white-space: pre-line;
}
/* Disable the submit button when Lexical is empty */
+ .comment__submit {
opacity: 0.5;
pointer-events: none;
}
}
}
+10 -1
View File
@@ -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,
+11 -1
View File
@@ -27,7 +27,9 @@ class CardsController < ApplicationController
end
def update
@card.update! card_params
suppressing_broadcasts_unless_published(@card) do
@card.update! card_params
end
if @card.published?
render_card_replacement
@@ -50,6 +52,14 @@ class CardsController < ApplicationController
@card = Current.user.accessible_cards.find params[:id]
end
def suppressing_broadcasts_unless_published(card, &block)
if card.published?
yield
else
Collection.suppressing_turbo_broadcasts(&block)
end
end
def card_params
params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ])
end
+3 -1
View File
@@ -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
+9 -1
View File
@@ -24,7 +24,7 @@ class UsersController < ApplicationController
def show
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
@day_timeline = Current.user.timeline_for(Time.current, filter: @filter)
@day_timeline = Current.user.timeline_for(day_param, filter: @filter)
end
def update
@@ -50,6 +50,14 @@ class UsersController < ApplicationController
head :forbidden unless Current.user.can_change?(@user)
end
def day_param
if params[:day].present?
Time.zone.parse(params[:day])
else
Time.current
end
end
def user_params
params.expect(user: [ :name, :email_address, :password, :avatar ])
end
+1 -1
View File
@@ -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([
-11
View File
@@ -52,17 +52,6 @@ module EventsHelper
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
end
def event_next_page_link(next_day)
if next_day
tag.div id: "next_page",
data: { controller: "fetch-on-visible",
fetch_on_visible_url_value: events_days_path(
day: next_day.strftime("%Y-%m-%d"),
**@filter.as_params
) }
end
end
def event_action_sentence(event)
if event.action.comment_created?
comment_event_action_sentence(event)
+24
View File
@@ -35,6 +35,30 @@ module PaginationHelper
end
end
def with_automatic_pagination(name, page, **properties)
pagination_list name, paginate_on_scroll: true, **properties do
concat(pagination_frame_tag(name, page) do
yield
concat link_to_next_page(name, page, activate_when_observed: true)
end)
end
end
def day_timeline_pagination_frame_tag(day_timeline, &)
turbo_frame_tag day_timeline_pagination_frame_id_for(day_timeline.day), data: { timeline_target: "frame" }, role: "presentation", refresh: :morph, &
end
def day_timeline_pagination_frame_id_for(day)
"day-timeline-pagination-contents-#{day.strftime("%Y-%m-%d")}"
end
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),
class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" }
end
end
private
def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block)
classes = properties.delete(:class)
@@ -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()
}
}
@@ -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)
}
+12 -2
View File
@@ -1,8 +1,8 @@
import { Controller } from "@hotwired/stimulus"
import { debounce } from "helpers/timing_helpers";
import { debounce, nextFrame } from "helpers/timing_helpers";
export default class extends Controller {
static targets = [ "cancel" ]
static targets = [ "cancel", "submit" ]
static values = {
debounceTimeout: { type: Number, default: 300 }
@@ -37,6 +37,16 @@ export default class extends Controller {
event.preventDefault()
}
async disableSubmitWhenInvalid(event) {
await nextFrame()
if (this.element.checkValidity()) {
this.submitTarget.removeAttribute("disabled")
} else {
this.submitTarget.toggleAttribute("disabled", true)
}
}
select(event) {
event.target.select()
}
@@ -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)
})
+1 -1
View File
@@ -4,7 +4,7 @@ class Account < ApplicationRecord
has_many_attached :uploads
def slug
"/#{tenant_id}"
"/#{tenant}"
end
def setup_basic_template
-1
View File
@@ -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
+1 -1
View File
@@ -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
+4
View File
@@ -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
+2 -2
View File
@@ -10,10 +10,10 @@
local_save_key_value: "comment-#{card.id}",
action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
<%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card),
data: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
<%= general_prompts(@card.collection) %>
<% end %>
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %>
<span>Post this comment</span>
<% end %>
<span data-form-target="cancel" hidden></span>
+1 -1
View File
@@ -27,7 +27,7 @@
</div>
<footer class="card__footer">
<%= render "cards/display/preview/meta", card: card %>
<%= render "cards/display/preview/meta", card: card, preview: true %>
<%= render "cards/display/common/background", card: card %>
</footer>
+3 -1
View File
@@ -27,6 +27,8 @@
<%= turbo_frame_tag :cards_container do %>
<section class="cards cards--grid">
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<%= with_automatic_pagination :cards_paginated_container, @page do %>
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
<% end %>
</section>
<% end %>
+1 -2
View File
@@ -3,7 +3,7 @@
<% content_for :header do %>
<%= render "filters/menu", user_filtering: @user_filtering %>
<div class="header__actions header__actions--start">
<%= link_to cards_path(collection_ids: [ @collection ]), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<%= link_to @collection, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
<span class="overflow-ellipsis">
&larr;
<strong><%= @collection.name %></strong>
@@ -38,7 +38,6 @@
</div>
<div class="settings__panel panel shadow center">
<%# render "collections/edit/workflows", collection: @collection %>
<%= render "collections/edit/auto_close", collection: @collection %>
<%= render "collections/edit/publication", collection: @collection %>
<%= render "collections/edit/delete", collection: @collection %>
+2
View File
@@ -1,6 +1,8 @@
<% @page_title = @collection.name %>
<% turbo_exempts_page_from_cache %>
<%= turbo_stream_from @collection %>
<% content_for :head do %>
<%= tag.meta property: "og:title", content: @page_title %>
<%= tag.meta property: "og:description", content: Account.sole.name %>
@@ -1,4 +1,4 @@
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open">
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
<span class="for-screen-reader">Column options</span>
@@ -1,4 +1,4 @@
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open:stop" aria-label="Add a new column, etc.">
<%= icon_tag "add", class: "translucent", "aria-hidden": true %>
</button>
+4
View File
@@ -0,0 +1,4 @@
<%= day_timeline_pagination_frame_tag @day_timeline do %>
<%= render "events/day", day_timeline: @day_timeline %>
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
<% end %>
@@ -1,7 +0,0 @@
<%= turbo_stream.append :activity do %>
<%= render "events/day", day_timeline: @day_timeline %>
<% end %>
<%= turbo_stream.replace :next_page do %>
<%= event_next_page_link(@day_timeline.next_day) %>
<% end %>
+5 -3
View File
@@ -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 };",
data: { related_element_target: "related",
related_element_group_value: card.id,
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
target: "_top",
data: {
related_element_target: "related",
related_element_group_value: card.id,
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
<div class="card__header flex align-start gap full-width">
<h4 class="card__collection flex-inline align-start">
<span class="card__id"><%= card.id %></span>
+11 -9
View File
@@ -1,20 +1,22 @@
<% @page_title = "Home" %>
<% @header_class = "header--events" %>
<%= turbo_stream_from :activity_summaries %>
<%= render "filters/broadcasts", filter: @filter %>
<% content_for :header do %>
<%= render "filters/menu", user_filtering: @user_filtering %>
<%= render "events/index/add_card_button", user_filtering: @user_filtering %>
<h1 class="header__title overflow-ellipsis">
<div class="overflow-ellipsis">
Activity in <%= @user_filtering.selected_collections_label %>
</div>
<h1 class="header__title">
<span>Activity in</span>
<%= render "events/index/filter", user_filtering: @user_filtering %>
</h1>
<% end %>
<div class="events" id="activity">
<%= render "events/day", day_timeline: @day_timeline %>
</div>
<%= event_next_page_link(@day_timeline.next_day) %>
<%= tag.div id: "activity", class: "events", data: { controller: "pagination", pagination_paginate_on_intersection_value: true } do %>
<%= day_timeline_pagination_frame_tag @day_timeline do %>
<%= render "events/day", day_timeline: @day_timeline %>
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
<% end %>
<% end %>
+43
View File
@@ -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 %>
<button type="button" class="btn borderless btn--plain events__filter-select" data-action="click->dialog#toggle:stop">
<span class="overflow-ellipsis" data-multi-selection-combobox-target="label">
</span>
</button>
<template data-multi-selection-combobox-target="hiddenFieldTemplate">
<%= hidden_field_tag "collection_ids[]", nil, data: { filter_settings_target: "field" } %>
</template>
<%= filter_dialog "Collection…" do %>
<strong class="popup__title">Collection…</strong>
<% 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 %>
<ul class="popup__list" data-filter-target="list" role="listbox">
<% user_filtering.collections.each do |collection| %>
<%= tag.li class: "popup__item", data: {
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: collection.id, multi_selection_combobox_label: collection.name },
role: "checkbox", aria: { checked: filter.collections.include?(collection) } do %>
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
<span class="overflow-ellipsis flex-item-grow"><%= collection.name %></span>
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
</button>
<% end %>
<% end %>
</ul>
<% end %>
<% end %>
<% end %>
+1 -1
View File
@@ -1,4 +1,4 @@
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
<%= render "filters/menu/button" %>
<%= tag.dialog class: "fizzy-menu filter popup popup--animated panel margin-block-start-half",
+4 -5
View File
@@ -1,12 +1,14 @@
<% cache user_filtering do %>
<%= tag.aside id: "filter-settings",
<%= tag.aside \
class: class_names("filters flex align-center gap-half justify-center center margin-block-end", { "filters--expanded": user_filtering.expanded? }),
data: {
controller: "toggle-class filter-settings",
toggle_class_toggle_class: "filters--expanded",
filter_settings_filters_set_class: "filters--has-filters-set",
filter_settings_no_filtering_url_value: no_filtering_url,
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",
@@ -14,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? %>
+1 -1
View File
@@ -1,6 +1,6 @@
<%= tag.button class:"fizzy-menu-trigger input input--select center flex-inline align-center txt-normal",
data: {
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
controller: "hotkey" } do %>
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
@@ -1,5 +1,5 @@
<% if platform.desktop? %>
<footer class="popup__footer">
Press <kbd class="kbd"><%= hotkey_label(["ctrl", "J"]) -%></kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move, <kbd class="kbd">enter</kbd> to navigate, <kbd class="kbd">SHIFT+ENTER</kbd> to select.
Press <kbd class="kbd">J</kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move, <kbd class="kbd">enter</kbd> to navigate.
</footer>
<% end %>
@@ -2,15 +2,8 @@
<% if Current.user.collections.many? %>
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" id="filter-collection-all">
<%= link_to clear_url_path, class: "popup__radio btn" do %>
<%= check_box_tag "filter_collections", nil, user_filtering.collections.blank?, class: "form-checkbox" %>
<span class="for-screen-reader">
See everything in all collections
</span>
<%= icon_tag "check", size: 18, class: "checked" %>
<% end %>
<%= link_to clear_url_path, class: "popup__btn btn" do %>
<%= icon_tag "collection", class: "popup__icon" %>
<%= link_to cards_path, class: "popup__btn btn" do %>
<span class="overflow-ellipsis">All collections</span>
<% end %>
</li>
@@ -1,17 +1,5 @@
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" id="filter-collection-<%= collection.id %>">
<label class="popup__radio btn">
<%= form.check_box "collection_ids[]", {
checked: user_filtering.filter.collections.include?(collection),
data: { action: "change->form#submit" },
include_hidden: false,
}, collection.id %>
<span class="for-screen-reader">
Toggle filter for <%= collection.name %>
</span>
<%= icon_tag "check", size: 18, class: "checked" %>
</label>
<%= icon_tag "collection", class: "popup__icon" %>
<%= link_to collection_path(collection), class: "popup__btn btn" do %>
<span class="overflow-ellipsis"><%= collection.name %></span>
<% end %>
@@ -2,8 +2,8 @@
<%= icon_tag "bookmark", class: "popup__icon" %>
<%= link_to cards_path(filter_id: filter.id), class: "popup__btn btn" do %>
<div class="txt-tight-lines min-width txt-small overflow-ellipsis">
<div><%= filter.collections_label %></div>
<div><%= filter.summary %></div>
<div><strong><%= filter.collections_label %></strong></div>
<div class="txt-capitalize"><%= filter.summary %></div>
</div>
<% end %>
</li>
@@ -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 %>
<button type="button" class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#toggle:stop">
<span class="overflow-ellipsis" data-multi-selection-combobox-target="label">
</span>
</button>
<template data-multi-selection-combobox-target="hiddenFieldTemplate">
<%= hidden_field_tag "collection_ids[]", nil, data: { filter_settings_target: "field" } %>
</template>
<%= filter_dialog "Collection…" do %>
<strong class="popup__title">Collection…</strong>
<% 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 %>
<ul class="popup__list" data-filter-target="list" role="listbox">
<% user_filtering.collections.each do |collection| %>
<%= tag.li class: "popup__item", data: {
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: collection.id, multi_selection_combobox_label: collection.name },
role: "checkbox", aria: { checked: filter.collections.include?(collection) } do %>
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change filter-settings#submitToGenericCardsView:stop">
<span class="overflow-ellipsis flex-item-grow"><%= collection.name %></span>
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
</button>
<% end %>
<% end %>
</ul>
<% end %>
<% end %>
@@ -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 %>
+3 -1
View File
@@ -19,7 +19,9 @@
<%= tenanted_action_cable_meta_tag %>
<%= yield :head %>
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
<% if ApplicationRecord.current_tenant %>
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
<% end %>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="icon" href="/favicon.png" type="image/png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
+15
View File
@@ -0,0 +1,15 @@
<% cache ApplicationRecord.tenants do %>
<h2>We're migrating Fizzy authentication</h2>
<p>
While we migrate Fizzy away from Launchpad and 37id, please login at one of the following URLs:
<ul style="list-style-type: none;">
<% ApplicationRecord.with_each_tenant do |tenant| %>
<li><%= link_to Account.sole.name, root_url(script_name: Account.sole.slug) %></li>
<% end %>
</ul>
</p>
<p>
Mike should have sent you your temporary password by now. If you haven't received it, please ping him!
</p>
<% end %>
@@ -0,0 +1,13 @@
<div class="events margin-block-double" id="activity" data-controller="pagination" data-pagination-paginate-on-intersection-value="true">
<%= day_timeline_pagination_frame_tag day_timeline do %>
<%= 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),
class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } %>
<% end %>
<% end %>
</div>
+1 -4
View File
@@ -50,7 +50,4 @@
<h1 class="font-weight-black txt-large margin-block-double"><%= "What #{ Current.user == @user ? "have you" : "has #{ @user.first_name }" } been up to?" %></h1>
<div class="events margin-block-double" id="activity">
<%= render "events/day", day_timeline: @day_timeline %>
</div>
<%= event_next_page_link(@day_timeline.next_day) %>
<%= render "users/activity_timeline", user: @user, day_timeline: @day_timeline, filter: @filter %>
+4 -6
View File
@@ -1,12 +1,10 @@
#!/usr/bin/env sh
echo "Login with david@37signals.com / secret123456 to:"
bin/rails runner - <<EOF
puts "Login with david@37signals.com / secret123456 to:"
ApplicationRecord.with_each_tenant do |tenant|
next unless tenant =~ /\A\d+\z/
if Account.sole.tenant_id
puts " - #{Account.sole.name}: http://fizzy.localhost:3006/#{Account.sole.tenant_id}"
end
puts " - #{Account.sole.name}: http://fizzy.localhost:3006#{Account.sole.slug}"
end
EOF
@@ -14,8 +12,8 @@ if [ -f tmp/solid-queue.txt ]; then
export SOLID_QUEUE_IN_PUMA=1
fi
if [ -f tmp/local-auth.txt ]; then
export LOCAL_AUTHENTICATION=1
if [ -f tmp/oss-config.txt ]; then
export OSS_CONFIG=1
fi
exec ./bin/rails server -p 3006
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env ruby
require_relative "../lib/bootstrap"
if !Bootstrap.local_authentication?
if !Bootstrap.oss_config?
# default from rails/test_unit/runner.rb but adding the saas gem test files
ENV["DEFAULT_TEST"] = "{gems/fizzy-saas/,}test/**/*_test.rb"
ENV["DEFAULT_TEST_EXCLUDE"] = "{gems/fizzy-saas/,}test/{system,dummy,fixtures}/**/*_test.rb"
+1 -1
View File
@@ -11,7 +11,7 @@ system("bundle config set --local auto_install true")
system("bundle check") || system!("bundle install")
require_relative "../lib/bootstrap"
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__) unless Bootstrap.local_authentication?
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__) unless Bootstrap.oss_config?
puts "\n== Preparing database =="
if ARGV.include?("--reset")
+4 -4
View File
@@ -9,10 +9,10 @@ CI.run do
step "Security: Importmap audit", "bin/importmap audit"
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
step "Tests: Rails with 37id auth", "bin/rails test"
step "Tests: Rails with local auth", "LOCAL_AUTHENTICATION=1 bin/rails test"
step "Tests: 37id", "bin/rails 37id:test:units"
step "Tests: System", "bin/rails test:system"
step "Tests: Rails: SaaS config", "bin/rails test"
step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test"
step "Tests: 37id", "bin/rails 37id:test:units"
step "Tests: System", "bin/rails test:system"
if success?
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-beta-lb-01
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-beta-lb-01
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-lb-101
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-lb-101
+12 -2
View File
@@ -39,11 +39,21 @@ accessories:
load-balancer:
image: basecamp/kamal-proxy:lb
roles:
- web
host: fizzy-staging-lb-01
options:
publish:
- 80:80
- 443:443
volumes:
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
otel_collector-load-balancer:
image: otel/opentelemetry-collector-contrib:0.126.0
port: 9394
files:
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock
options:
user: 0
host: fizzy-staging-lb-01
+1 -1
View File
@@ -1,2 +1,2 @@
require "bootstrap"
Rails.application.config.x.local_authentication = Bootstrap.local_authentication?
Rails.application.config.x.oss_config = Bootstrap.oss_config?
+1 -1
View File
@@ -221,7 +221,7 @@ Rails.application.routes.draw do
root "events#index"
unless Rails.application.config.x.local_authentication
unless Rails.application.config.x.oss_config
mount Fizzy::Saas::Engine, at: "/", as: "saas"
end
+11 -40
View File
@@ -13,7 +13,7 @@ end
def create_tenant(signal_account_name, bare: false)
if bare
tenant_id = Digest::SHA256.hexdigest(signal_account_name)[0..8].to_i(16)
elsif Rails.application.config.x.local_authentication
elsif Rails.application.config.x.oss_config
tenant_id = ActiveRecord::FixtureSet.identify signal_account_name
else
signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name)
@@ -22,7 +22,7 @@ def create_tenant(signal_account_name, bare: false)
ApplicationRecord.destroy_tenant tenant_id
ApplicationRecord.create_tenant(tenant_id) do
account = if bare || Rails.application.config.x.local_authentication
account = if bare || Rails.application.config.x.oss_config
Account.create(name: signal_account_name, tenant_id: tenant_id).tap do
User.create!(
name: "David Heinemeier Hansson",
@@ -40,45 +40,16 @@ def create_tenant(signal_account_name, bare: false)
end
def find_or_create_user(full_name, email_address)
if Rails.application.config.x.local_authentication
if user = User.find_by(email_address: email_address)
user.password = "secret123456"
user.save!
user
else
User.create!(
name: full_name,
email_address: email_address,
password: "secret123456"
)
end
if user = User.find_by(email_address: email_address)
user.password = "secret123456"
user.save!
user
else
SignalId::Database.on_master do
unless signal_identity = SignalId::Identity.find_by_email_address(email_address)
signal_identity = SignalId::Identity.create!(
name: full_name,
email_address: email_address,
username: email_address,
password: "secret123456"
)
end
signal_account = Account.sole.external_account
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
if user = User.find_by(external_user_id: signal_user.id)
user.password = "secret123456"
user.save!
user
else
User.create!(
external_user_id: signal_user.id,
name: signal_identity.name,
email_address: signal_identity.email_address,
password: "secret123456"
)
end
end
User.create!(
name: full_name,
email_address: email_address,
password: "secret123456"
)
end
end
@@ -1,14 +0,0 @@
module Sessions
module SignalSessions
extend ActiveSupport::Concern
included do
before_action :require_local_auth, only: %i[ new create ]
end
private
def require_local_auth
head :forbidden
end
end
end
@@ -1,23 +0,0 @@
class Sessions::LaunchpadController < ApplicationController
require_unauthenticated_access
before_action :require_sig
def show
end
def update
user = Account.sole.external_account.authenticate(sig: @sig).try(:peer)
if user.present? && user.active?
start_new_session_for user
redirect_to after_authentication_url
else
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
end
end
private
def require_sig
@sig = params.expect(:sig)
end
end
@@ -1,15 +0,0 @@
module LoginHelper
module SignalLogin
def login_url
if ApplicationRecord.current_tenant
Launchpad.login_url(product: true, account: Account.sole)
else
Launchpad.login_url(product: true)
end
end
def logout_url
Launchpad.logout_url
end
end
end
@@ -20,9 +20,8 @@ module Account::SignalAccount
User.create!(
name: account.external_account.owner.name,
email_address: account.external_account.owner.email_address,
external_user_id: account.external_account.owner.id,
role: "admin",
password: SecureRandom.hex(36) # TODO: remove password column?
password: SecureRandom.hex(16)
)
end
end
@@ -1,19 +0,0 @@
module User::SignalUser
extend ActiveSupport::Concern
prepended do
# the external_user_id is the SignalId::User id
belongs_to :external_user, dependent: :destroy, class_name: "SignalId::User", optional: true
end
class_methods do
def find_by_signal_user_id(signal_user_id)
find_by(external_user_id: signal_user_id)
end
end
def deactivate
super
SignalId::Database.on_master { external_user&.destroy }
end
end
-6
View File
@@ -1,10 +1,4 @@
Fizzy::Saas::Engine.routes.draw do
resource :session do
scope module: "sessions" do
resource :launchpad, only: %i[ show update ], controller: "launchpad"
end
end
namespace :signup do
get "/" => "accounts#new"
resources :accounts, only: %i[ new create ]
-4
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
-4
View File
@@ -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
+2 -2
View File
@@ -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
-33
View File
@@ -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} <email> <tenant>"
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
+14 -30
View File
@@ -2,41 +2,25 @@
require_relative "../config/environment"
if ARGV.length < 2
puts "Usage: #{$0} <email> <tenant>"
if ARGV.length < 3
puts "Usage: #{$0} <tenant> <email> <fullname>"
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
-1
View File
@@ -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
@@ -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
@@ -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
-41
View File
@@ -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
@@ -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
+19 -21
View File
@@ -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
-9
View File
@@ -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
+1 -1
View File
@@ -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
+16 -19
View File
@@ -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
+54 -57
View File
@@ -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
+1 -1
View File
@@ -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
+1 -5
View File
@@ -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"