Merge branch 'main' into search-page-markup
* main: (67 commits) Add jason@37signals.com to the seeds. Rework menu sizing Firefox has its own psuedo elements Allow pop-ups to overflow without breaking the layout Style the prompt empty state Update lexical Scope CSS to custom attachments Update to point to version with fix for attachments Update AR::Tenanted to address tenant creation raciness dep: Update signal_id to quash noisy test warnings Update dependences, including Rails and queenbee-plugin Update 400/422/500 error pages with latest stock Rails versions Remove subdomain listing from the 404 page Delete the first runs controller/routes/model Redirect unauthenticated users to Launchpad Update a few users' email to link to a valid identity. Make sure the beta env keeps local authentication (for now) Trampoline the Launchpad auth redirect from a GET into a POST Add the shared signal_id secret Script to create QB and SignalID accounts, and SignalID users ...
This commit is contained in:
+3
-2
@@ -19,7 +19,8 @@ FROM base AS build
|
||||
|
||||
# Install packages needed to build gems
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev
|
||||
apt-get install -y --no-install-recommends -y build-essential git libvips pkg-config libyaml-dev default-libmysqlclient-dev && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Install application gems
|
||||
COPY Gemfile Gemfile.lock .ruby-version ./
|
||||
@@ -47,7 +48,7 @@ FROM base
|
||||
|
||||
# Install packages needed for deployment
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools && \
|
||||
apt-get install --no-install-recommends -y curl libsqlite3-0 libvips build-essential ffmpeg groff libreoffice-writer libreoffice-impress libreoffice-calc mupdf-tools default-libmysqlclient-dev && \
|
||||
rm -rf /var/lib/apt/lists /var/cache/apt/archives
|
||||
|
||||
# Copy built artifacts: gems, application
|
||||
|
||||
@@ -32,6 +32,16 @@ gem "image_processing", "~> 1.14"
|
||||
gem "platform_agent"
|
||||
gem "aws-sdk-s3", require: false
|
||||
|
||||
# 37id and Queenbee integration
|
||||
gem "signal_id", bc: "signal_id", branch: "rails4"
|
||||
gem "mysql2" # needed by signal_id
|
||||
gem "queuety", bc: "queuety", branch: "rails4" # needed by signal_id
|
||||
gem "service_concurrency_prevention", bc: "service_concurrency_prevention" # needed by queuety
|
||||
gem "portfolio", ">= 4.6", bc: "portfolio" # needed by signal_id
|
||||
gem "file_repository", "~> 1.4.5", bc: "file_repository" # needed by portfolio
|
||||
gem "queenbee", bc: "queenbee-plugin"
|
||||
gem "activeresource", require: "active_resource" # needed by queenbee
|
||||
|
||||
# Telemetry, logging, and operations
|
||||
gem "mission_control-jobs"
|
||||
gem "sentry-ruby"
|
||||
@@ -55,4 +65,5 @@ group :test do
|
||||
gem "selenium-webdriver"
|
||||
gem "webmock"
|
||||
gem "vcr"
|
||||
gem "mocha"
|
||||
end
|
||||
|
||||
+102
-14
@@ -1,13 +1,13 @@
|
||||
GIT
|
||||
remote: https://github.com/basecamp/actiontext-lexical
|
||||
revision: 3f1ec3916f468ac2bb626091bcb97469c2c52483
|
||||
revision: 79ac1958183afb1d8cecd6799d230b63392eb014
|
||||
specs:
|
||||
actiontext-lexical (0.1.0)
|
||||
rails (>= 8.0.2)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/active_record-tenanted
|
||||
revision: f6fca189655ae6609d10ecad7cceed61592320c4
|
||||
revision: 81a4d02a363fa749de517c90e19588365f934913
|
||||
specs:
|
||||
active_record-tenanted (0.1.0)
|
||||
activerecord (>= 8.1.alpha)
|
||||
@@ -15,6 +15,12 @@ GIT
|
||||
railties (>= 8.1.alpha)
|
||||
zeitwerk
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/file_repository
|
||||
revision: 17e90fc35ccb9f0a0dae1ed8623f01386696002d
|
||||
specs:
|
||||
file_repository (1.4.5)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/kamal
|
||||
revision: 344e2d7995e0134aa52f195a984d8d5e24e31d94
|
||||
@@ -32,6 +38,32 @@ GIT
|
||||
thor (~> 1.3)
|
||||
zeitwerk (>= 2.6.18, < 3.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/portfolio
|
||||
revision: 26dd0ccbe4aad5a5c539f90c00e82b6175443636
|
||||
specs:
|
||||
portfolio (5.2.0)
|
||||
activesupport
|
||||
file_repository (>= 1.4.4)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/queenbee-plugin
|
||||
revision: eb476e7f309b7642aa20254f0bdbadcec1dda2b2
|
||||
specs:
|
||||
queenbee (3.2.0)
|
||||
activeresource
|
||||
builder
|
||||
rexml
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/queuety
|
||||
revision: bf7e2a552fb674533d3e092f06dbf9b5d1ebc753
|
||||
branch: rails4
|
||||
specs:
|
||||
queuety (4.0.2)
|
||||
activerecord
|
||||
service_concurrency_prevention
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/rails-structured-logging
|
||||
revision: 76960cb5c15fc2b6b5f7542e05d7dcc031cef9e6
|
||||
@@ -40,6 +72,29 @@ GIT
|
||||
json
|
||||
rails (>= 6.0.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/service_concurrency_prevention
|
||||
revision: 320da99431718615ed86f8723e8b9d3259c602a3
|
||||
specs:
|
||||
service_concurrency_prevention (1.1.1)
|
||||
activesupport
|
||||
|
||||
GIT
|
||||
remote: https://github.com/basecamp/signal_id
|
||||
revision: 8908b2550ff55325b04c108146e00b4408a64e93
|
||||
branch: rails4
|
||||
specs:
|
||||
signal_id (4.3.2)
|
||||
bcrypt-ruby (>= 3.0.0)
|
||||
google-id-token (>= 1.4.0)
|
||||
mocha
|
||||
multi_json
|
||||
portfolio (>= 4.0)
|
||||
pwned (~> 2.0)
|
||||
queuety (>= 3.0)
|
||||
rails (>= 4.0.2)
|
||||
rails-controller-testing
|
||||
|
||||
GIT
|
||||
remote: https://github.com/crmne/ruby_llm.git
|
||||
revision: a35aa111a0bb36b7f0363b1f1f2a25e3d5378fd3
|
||||
@@ -56,7 +111,7 @@ GIT
|
||||
|
||||
GIT
|
||||
remote: https://github.com/rails/rails.git
|
||||
revision: fd49d6a3180244969fb5ab9768d0bc7d9e4df9fe
|
||||
revision: 53a9a3181a59f4e6dff73ba9453734edd7f7e076
|
||||
branch: main
|
||||
specs:
|
||||
actioncable (8.1.0.alpha)
|
||||
@@ -159,22 +214,30 @@ GEM
|
||||
specs:
|
||||
action_text-trix (2.1.15)
|
||||
railties
|
||||
activemodel-serializers-xml (1.0.3)
|
||||
activemodel (>= 5.0.0.a)
|
||||
activesupport (>= 5.0.0.a)
|
||||
builder (~> 3.1)
|
||||
activeresource (6.1.4)
|
||||
activemodel (>= 6.0)
|
||||
activemodel-serializers-xml (~> 1.0)
|
||||
activesupport (>= 6.0)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
ast (2.4.3)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1115.0)
|
||||
aws-sdk-core (3.225.2)
|
||||
aws-partitions (1.1119.0)
|
||||
aws-sdk-core (3.226.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.104.0)
|
||||
aws-sdk-kms (1.105.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.189.1)
|
||||
aws-sdk-s3 (1.190.0)
|
||||
aws-sdk-core (~> 3, >= 3.225.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
@@ -182,6 +245,8 @@ GEM
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
base64 (0.3.0)
|
||||
bcrypt (3.1.20)
|
||||
bcrypt-ruby (3.1.5)
|
||||
bcrypt (>= 3.1.3)
|
||||
bcrypt_pbkdf (1.1.1)
|
||||
benchmark (0.4.1)
|
||||
bigdecimal (3.2.2)
|
||||
@@ -210,7 +275,7 @@ GEM
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
date (3.4.1)
|
||||
debug (1.10.0)
|
||||
debug (1.11.0)
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
dotenv (3.1.8)
|
||||
@@ -243,6 +308,8 @@ GEM
|
||||
addressable (>= 2.5.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-id-token (1.4.2)
|
||||
jwt (>= 1)
|
||||
hashdiff (1.2.0)
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -263,6 +330,8 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.12.2)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
@@ -292,11 +361,15 @@ GEM
|
||||
railties (>= 7.1)
|
||||
stimulus-rails
|
||||
turbo-rails
|
||||
mocha (2.7.1)
|
||||
ruby2_keywords (>= 0.0.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mysql2 (0.5.6)
|
||||
net-http (0.6.0)
|
||||
uri
|
||||
net-imap (0.5.8)
|
||||
net-imap (0.5.9)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -343,6 +416,7 @@ GEM
|
||||
public_suffix (6.0.2)
|
||||
puma (6.6.0)
|
||||
nio4r (~> 2.0)
|
||||
pwned (2.4.1)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.16)
|
||||
@@ -355,6 +429,10 @@ GEM
|
||||
rack (>= 1.3)
|
||||
rackup (2.2.1)
|
||||
rack (>= 3)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
activesupport (>= 5.0.1.rc1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -365,7 +443,7 @@ GEM
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.0)
|
||||
rb_sys (0.9.106)
|
||||
rdoc (6.14.0)
|
||||
rdoc (6.14.1)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
redcarpet (3.6.1)
|
||||
@@ -410,6 +488,7 @@ GEM
|
||||
ruby-vips (2.2.3)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.33.0)
|
||||
@@ -461,10 +540,10 @@ GEM
|
||||
railties (>= 6.0.0)
|
||||
stringio (3.1.7)
|
||||
thor (1.3.2)
|
||||
thruster (0.1.13)
|
||||
thruster (0.1.13-arm64-darwin)
|
||||
thruster (0.1.13-x86_64-darwin)
|
||||
thruster (0.1.13-x86_64-linux)
|
||||
thruster (0.1.14)
|
||||
thruster (0.1.14-arm64-darwin)
|
||||
thruster (0.1.14-x86_64-darwin)
|
||||
thruster (0.1.14-x86_64-linux)
|
||||
tiktoken_ruby (0.0.11.1)
|
||||
rb_sys (= 0.9.106)
|
||||
tiktoken_ruby (0.0.11.1-arm64-darwin)
|
||||
@@ -506,6 +585,7 @@ PLATFORMS
|
||||
DEPENDENCIES
|
||||
actiontext-lexical!
|
||||
active_record-tenanted!
|
||||
activeresource
|
||||
aws-sdk-s3
|
||||
bcrypt (~> 3.1.7)
|
||||
bootsnap
|
||||
@@ -513,15 +593,21 @@ DEPENDENCIES
|
||||
bundler-audit
|
||||
capybara
|
||||
debug
|
||||
file_repository (~> 1.4.5)!
|
||||
geared_pagination (~> 1.2)
|
||||
image_processing (~> 1.14)
|
||||
importmap-rails
|
||||
jbuilder
|
||||
kamal!
|
||||
mission_control-jobs
|
||||
mocha
|
||||
mysql2
|
||||
platform_agent
|
||||
portfolio (>= 4.6)!
|
||||
propshaft
|
||||
puma (>= 5.0)
|
||||
queenbee!
|
||||
queuety!
|
||||
rails!
|
||||
rails_structured_logging!
|
||||
redcarpet
|
||||
@@ -532,6 +618,8 @@ DEPENDENCIES
|
||||
selenium-webdriver
|
||||
sentry-rails
|
||||
sentry-ruby
|
||||
service_concurrency_prevention!
|
||||
signal_id!
|
||||
solid_cable (>= 3.0)
|
||||
solid_cache (~> 1.0)
|
||||
solid_queue (~> 1.1)
|
||||
|
||||
@@ -1,136 +1,201 @@
|
||||
lexical-editor {
|
||||
.node--selected {
|
||||
&:not(:has(img)) {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
@layer components {
|
||||
lexical-editor {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
.node--selected:not(.attachment--custom) {
|
||||
&:not(:has(img)) {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
}
|
||||
|
||||
&:has(img) {
|
||||
img {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:has(img) {
|
||||
img {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
|
||||
&.lexical-editor--empty {
|
||||
.lexical-editor__content::before {
|
||||
content: attr(placeholder);
|
||||
color: currentColor;
|
||||
cursor: text;
|
||||
opacity: 0.66;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.lexical-editor--empty {
|
||||
.lexical-editor__content::before {
|
||||
content: attr(placeholder);
|
||||
color: currentColor;
|
||||
cursor: text;
|
||||
opacity: 0.66;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
white-space: pre-line;
|
||||
.lexical-dialog-actions {
|
||||
display: flex;
|
||||
font-size: var(--text-x-small);
|
||||
flex: 1 1 0%;
|
||||
gap: var(--inline-space-half);
|
||||
margin-block-start: var(--block-space-half);
|
||||
|
||||
.btn {
|
||||
--radius: 0.3em;
|
||||
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
|
||||
&:is([type="submit"]) {
|
||||
--btn-background: var(--card-color, var(--color-link));
|
||||
--btn-color: var(--color-ink-inverted);
|
||||
--outline-color: var(--card-color, var(--color-link));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-dialog-actions {
|
||||
display: flex;
|
||||
font-size: var(--text-x-small);
|
||||
flex: 1 1 0%;
|
||||
gap: var(--inline-space-half);
|
||||
margin-block-start: var(--block-space-half);
|
||||
|
||||
.btn {
|
||||
--radius: 0.3em;
|
||||
|
||||
inline-size: 100%;
|
||||
justify-content: center;
|
||||
|
||||
&:is([type="submit"]) {
|
||||
--btn-background: var(--card-color, var(--color-link));
|
||||
--btn-color: var(--color-ink-inverted);
|
||||
--outline-color: var(--card-color, var(--color-link));
|
||||
span {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
.lexical-editor__content {
|
||||
--outline-size: max(2px, 0.08em);
|
||||
|
||||
.lexical-editor__content {
|
||||
--outline-size: max(2px, 0.08em);
|
||||
margin: var(--block-space-half) 0;
|
||||
min-block-size: calc(7lh + var(--block-space));
|
||||
outline: 0;
|
||||
padding: var(--block-space-half) var(--inline-space);
|
||||
|
||||
margin: var(--block-space-half) 0;
|
||||
min-block-size: calc(7lh + var(--block-space));
|
||||
outline: 0;
|
||||
padding: var(--block-space-half) var(--inline-space);
|
||||
* {
|
||||
&:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
&:first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lexical-editor--drag-over {
|
||||
background-color: var(--color-selected);
|
||||
border-radius: 4px;
|
||||
outline: 2px dashed var(--color-selected-dark);
|
||||
}
|
||||
|
||||
lexical-toolbar {
|
||||
background-color: inherit;
|
||||
border-block-end: 1px solid var(--color-ink-light);
|
||||
color: currentColor;
|
||||
display: flex;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: 0.2em 0;
|
||||
position: relative;
|
||||
|
||||
dialog {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.5em;
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--color-ink);
|
||||
padding: var(--block-space) calc(var(--inline-space) * 1.5);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
.input[type="url"] {
|
||||
min-inline-size: 30ch;
|
||||
&:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
aspect-ratio: 4/3.5;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
block-size: 2em;
|
||||
border: none;
|
||||
border-radius: 0.2em;
|
||||
.lexical-editor--drag-over {
|
||||
background-color: var(--color-selected);
|
||||
border-radius: 4px;
|
||||
outline: 2px dashed var(--color-selected-dark);
|
||||
}
|
||||
|
||||
lexical-toolbar {
|
||||
background-color: inherit;
|
||||
border-block-end: 1px solid var(--color-ink-light);
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
display: flex;
|
||||
font-size: inherit;
|
||||
inline-size: auto;
|
||||
place-items: center;
|
||||
transition: background-color 300ms ease;
|
||||
margin: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: 0.2em 0;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
-webkit-touch-callout: none;
|
||||
block-size: 0.85em;
|
||||
fill: currentColor;
|
||||
grid-area: 1/1;
|
||||
dialog {
|
||||
background-color: var(--color-canvas);
|
||||
border: 1px solid var(--color-ink-lighter);
|
||||
border-radius: 0.5em;
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--color-ink);
|
||||
padding: var(--block-space) calc(var(--inline-space) * 1.5);
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
.input[type="url"] {
|
||||
min-inline-size: 30ch;
|
||||
}
|
||||
}
|
||||
|
||||
> button {
|
||||
aspect-ratio: 4/3.5;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
block-size: 2em;
|
||||
border: none;
|
||||
border-radius: 0.2em;
|
||||
color: currentColor;
|
||||
cursor: pointer;
|
||||
display: grid;
|
||||
font-size: inherit;
|
||||
inline-size: auto;
|
||||
user-select: none;
|
||||
}
|
||||
place-items: center;
|
||||
transition: background-color 300ms ease;
|
||||
|
||||
&:is(:focus, :hover) {
|
||||
background-color: var(--color-ink-lighter);
|
||||
box-shadow: none;
|
||||
}
|
||||
svg {
|
||||
-webkit-touch-callout: none;
|
||||
block-size: 0.85em;
|
||||
fill: currentColor;
|
||||
grid-area: 1/1;
|
||||
inline-size: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&:is(:active) {
|
||||
&:is(:focus, :hover) {
|
||||
background-color: var(--color-ink-lighter);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:is(:active) {
|
||||
background-color: var(--color-selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Prompt
|
||||
/* ------------------------------------------------------------------------ */
|
||||
|
||||
.lexical-prompt-menu {
|
||||
--lexical-prompt-avatar-size: 24px;
|
||||
--lexical-prompt-min-width: 20ch;
|
||||
--lexical-prompt-padding: 0.5ch;
|
||||
|
||||
background-color: var(--color-canvas);
|
||||
border-radius: calc(var(--lexical-prompt-padding) * 2);
|
||||
box-shadow: var(--shadow);
|
||||
font-size: var(--text-small);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
min-inline-size: var(--lexical-prompt-min-width);
|
||||
padding: var(--lexical-prompt-padding);
|
||||
visibility: hidden;
|
||||
z-index: var(--z-popup);
|
||||
}
|
||||
|
||||
.lexical-prompt-menu--visible {
|
||||
visibility: initial;
|
||||
}
|
||||
|
||||
.lexical-prompt-menu__item {
|
||||
align-items: center;
|
||||
border-radius: 0.5ch;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: var(--lexical-prompt-padding);
|
||||
padding: var(--lexical-prompt-padding);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-selected);
|
||||
}
|
||||
|
||||
&[aria-selected] {
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset;
|
||||
|
||||
&:hover {
|
||||
--hover-size: 0;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
block-size: var(--lexical-prompt-avatar-size);
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
inline-size: var(--lexical-prompt-avatar-size);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.lexical-prompt-menu__item--empty {
|
||||
color: var(--color-ink-medium);
|
||||
padding: var(--lexical-prompt-padding);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
border-radius: 0.3em;
|
||||
box-shadow: 0 0.1em 0 currentColor;
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-x-small);
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
padding: 0 0.4em;
|
||||
|
||||
@@ -225,43 +225,5 @@
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btn__group--chunky {
|
||||
--gap: 0.2em;
|
||||
|
||||
gap: var(--gap);
|
||||
margin: var(--block-space-half) auto calc(var(--block-space-half) / 2);
|
||||
|
||||
.btn {
|
||||
--btn-border-radius: 0.4em;
|
||||
|
||||
align-content: end;
|
||||
aspect-ratio: 5/3;
|
||||
flex-basis: calc(33% - var(--gap) / 2);
|
||||
flex-direction: column;
|
||||
font-size: var(--text-small);
|
||||
line-height: 1;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
row-gap: 0.3lh;
|
||||
text-align: center;
|
||||
|
||||
kbd {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
inset: 0.66em 0.33em auto auto;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon {
|
||||
--icon-size: 2em !important;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
}
|
||||
|
||||
.card__content {
|
||||
contain: inline-size;
|
||||
flex: 2 1 auto;
|
||||
max-inline-size: 100%;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@layer components {
|
||||
:is(.dialog) {
|
||||
--backdrop-speed: 150ms;
|
||||
--panel-size: max-content;
|
||||
--speed: 150ms;
|
||||
|
||||
border: 0;
|
||||
|
||||
@@ -1,28 +1,72 @@
|
||||
.fizzy-dialog {
|
||||
--panel-border-color: var(--color-selected-dark);
|
||||
--panel-border-radius: 1.4em;
|
||||
@layer components {
|
||||
.fizzy-dialog:is(.panel) {
|
||||
--panel-border-color: var(--color-selected-dark);
|
||||
--panel-border-radius: 1.4em;
|
||||
--panel-size: 45ch;
|
||||
|
||||
box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
|
||||
z-index: 5;
|
||||
}
|
||||
box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
|
||||
}
|
||||
|
||||
.fizzy-menu {
|
||||
--input-border-color: var(--color-selected-dark);
|
||||
--input-padding: 0.2em 2em 0.2em 0.9em;
|
||||
.input:is(.fizzy-menu) {
|
||||
--input-border-color: var(--color-selected-dark);
|
||||
--input-padding: 0.2em 2em 0.2em 0.9em;
|
||||
|
||||
box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
|
||||
margin-block-start: 0.1em;
|
||||
box-shadow: 0 0 0 1px oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.2em 0.2em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.4em 0.4em oklch(var(--lch-blue-medium) / 5%),
|
||||
0 0.8em 0.8em oklch(var(--lch-blue-medium) / 5%);
|
||||
margin-block-start: 0.1em;
|
||||
|
||||
svg {
|
||||
block-size: auto;
|
||||
inline-size: 1.3em;
|
||||
margin-block-end: 0.2em;
|
||||
margin-inline-end: 0.8ch;
|
||||
svg {
|
||||
block-size: auto;
|
||||
inline-size: 1.3em;
|
||||
margin-block-end: 0.2em;
|
||||
margin-inline-end: 0.8ch;
|
||||
}
|
||||
}
|
||||
|
||||
.fizzy-menu__hotkeys {
|
||||
--gap: 8px;
|
||||
|
||||
gap: var(--gap);
|
||||
margin: var(--block-space) auto calc(var(--block-space-half) / 2);
|
||||
|
||||
.btn {
|
||||
--btn-border-radius: 0.4em;
|
||||
|
||||
align-content: end;
|
||||
aspect-ratio: 5/3;
|
||||
background-color: var(--color-ink-lightest);
|
||||
border-radius: 0.5em;
|
||||
flex-basis: calc((100% - var(--gap) * 2) / 3);
|
||||
flex-direction: column;
|
||||
font-size: var(--text-small);
|
||||
line-height: 1;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
row-gap: 0.3lh;
|
||||
text-align: center;
|
||||
|
||||
kbd {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
inset: 0.66em 0.33em auto auto;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.icon {
|
||||
--icon-size: 2em !important;
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,22 @@
|
||||
translate: -50% -50%;
|
||||
z-index: 1;
|
||||
|
||||
&::-moz-range-track {
|
||||
block-size: var(--knob-size);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
block-size: var(--knob-size);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
@layer components {
|
||||
.popup {
|
||||
--panel-border-radius: 0.5em;
|
||||
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space);
|
||||
--panel-padding: var(--block-space);
|
||||
--panel-size: auto;
|
||||
|
||||
inline-size: auto;
|
||||
inset: 0 auto auto 50%;
|
||||
max-block-size: 80vh;
|
||||
min-inline-size: min(25ch, 90vw);
|
||||
max-inline-size: min(55ch, 90vw);
|
||||
min-inline-size: min(25ch, calc(100dvw - var(--block-space-double)));
|
||||
max-inline-size: min(55ch, calc(100dvw - var(--block-space-double)));
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
@@ -18,15 +17,14 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:has(.popup__footer) {
|
||||
--panel-padding: var(--block-space) var(--block-space) 0 var(--block-space)
|
||||
}
|
||||
|
||||
#header:has(&) {
|
||||
max-inline-size: 100vw;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
|
||||
.popup {
|
||||
min-inline-size: 40ch;
|
||||
max-inline-size: 40ch;
|
||||
}
|
||||
}
|
||||
|
||||
.card & {
|
||||
@@ -37,14 +35,19 @@
|
||||
|
||||
.popup__footer {
|
||||
background-color: var(--color-canvas);
|
||||
inset: auto 0 0 0;
|
||||
padding: var(--block-space);
|
||||
border-block-start: 1px solid var(--color-ink-lighter);
|
||||
font-size: var(--text-small);
|
||||
inset: auto 0 0;
|
||||
margin-inline: calc(var(--block-space) * -1);
|
||||
padding: var(--block-space-half) var(--inline-space) var(--block-space);
|
||||
position: sticky;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.popup__group {
|
||||
background: var(--color-ink-lightest);
|
||||
background: transparent;
|
||||
border-radius: 0.3em;
|
||||
max-inline-size: 100%;
|
||||
padding-inline: var(--inline-space);
|
||||
|
||||
@@ -61,13 +64,12 @@
|
||||
}
|
||||
|
||||
&[aria-selected] {
|
||||
border-radius: 0.3em;
|
||||
box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset;
|
||||
}
|
||||
}
|
||||
|
||||
.popup__item {
|
||||
--btn-background: var(--color-ink-lightest);
|
||||
--btn-background: transparent;
|
||||
--btn-border-radius: 0.3em;
|
||||
--btn-border-size: 0;
|
||||
--btn-font-weight: 500;
|
||||
@@ -114,27 +116,39 @@
|
||||
}
|
||||
|
||||
.popup__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inline-size: 100%;
|
||||
margin: 0;
|
||||
max-inline-size: 100%;
|
||||
padding: 0;
|
||||
row-gap: 2px;
|
||||
}
|
||||
|
||||
.popup__new {
|
||||
--btn-icon-size: 1em;
|
||||
|
||||
padding-inline-start: var(--inline-space-half);
|
||||
|
||||
.icon {
|
||||
margin-inline-end: 0.5em;
|
||||
}
|
||||
|
||||
padding-inline-start: var(--inline-space-half);
|
||||
}
|
||||
|
||||
.popup__title {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.popup__group-title {
|
||||
font-size: var(--text-small);
|
||||
margin-block: var(--block-space-half);
|
||||
padding-inline: var(--inline-space);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.popup--animated {
|
||||
--backdrop-speed: 150ms;
|
||||
--panel-size: max-content;
|
||||
--speed: 150ms;
|
||||
|
||||
opacity: 0;
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
:where(h5) { font-size: 0.83em; }
|
||||
:where(h6) { font-size: 0.67em; }
|
||||
|
||||
:where(p, ul, ol, dl, blockquote, figure, .attachment) {
|
||||
:where(p, ul, ol, dl, blockquote, figure:not(.attachment--custom), .attachment:not(.attachment--custom)) {
|
||||
margin-block: var(--block-margin);
|
||||
overflow-wrap: break-word;
|
||||
text-wrap: pretty;
|
||||
@@ -54,7 +54,7 @@
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachment {
|
||||
.attachment:not(.attachment--custom) {
|
||||
block-size: auto;
|
||||
display: inline-block;
|
||||
inline-size: 100%;
|
||||
@@ -220,15 +220,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Attachment junk gets wrapped in a paragraph causing unwanted space */
|
||||
p:has(action-text-attachment) {
|
||||
display: none;
|
||||
|
||||
+ * {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Avoid extra space due to empty paragraphs */
|
||||
p:empty {
|
||||
display: none;
|
||||
@@ -329,5 +320,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom attachments such as mentions, etc. */
|
||||
.attachment--custom {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.attachment--custom,
|
||||
action-text-attachment[content-type^='application/vnd.actiontext'] {
|
||||
--attachment-image-size: 1em;
|
||||
--attachment-selected-color: oklch(var(--lch-blue-dark));
|
||||
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 0.25ch;
|
||||
position: relative;
|
||||
vertical-align: bottom;
|
||||
|
||||
lexical-editor & {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
block-size: var(--attachment-image-size);
|
||||
border-radius: 50%;
|
||||
inline-size: var(--attachment-image-size);
|
||||
}
|
||||
|
||||
&.node--selected {
|
||||
background: var(--attachment-selected-color);
|
||||
box-shadow:
|
||||
-0.25ch 0 0 var(--attachment-selected-color),
|
||||
0.5ch 0 0 var(--attachment-selected-color);
|
||||
border-radius: 99rem;
|
||||
color: var(--color-ink-inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class ApplicationController < ActionController::Base
|
||||
include Authentication, CurrentTimezone, SetPlatform, WriterAffinity
|
||||
include Authentication, CurrentRequest, CurrentTimezone, SetPlatform, WriterAffinity
|
||||
|
||||
stale_when_importmap_changes
|
||||
allow_browser versions: :modern
|
||||
|
||||
@@ -2,6 +2,7 @@ module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_current_account
|
||||
before_action :require_authentication
|
||||
helper_method :authenticated?
|
||||
end
|
||||
@@ -16,6 +17,12 @@ module Authentication
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :resume_session, **options
|
||||
end
|
||||
|
||||
def require_untenanted_access(**options)
|
||||
skip_before_action :set_current_account, **options
|
||||
skip_before_action :require_authentication, **options
|
||||
before_action :redirect_tenanted_request, **options
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -40,7 +47,12 @@ module Authentication
|
||||
|
||||
def request_authentication
|
||||
session[:return_to_after_authenticating] = request.url
|
||||
redirect_to new_session_path
|
||||
|
||||
if Rails.application.config.x.local_authentication
|
||||
redirect_to new_session_path
|
||||
else
|
||||
redirect_to Launchpad.login_url(account: Current.account), allow_other_host: true
|
||||
end
|
||||
end
|
||||
|
||||
def after_authentication_url
|
||||
@@ -51,6 +63,9 @@ module Authentication
|
||||
redirect_to root_url if authenticated?
|
||||
end
|
||||
|
||||
def redirect_tenanted_request
|
||||
redirect_to root_url if ApplicationRecord.current_tenant
|
||||
end
|
||||
|
||||
def start_new_session_for(user)
|
||||
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
|
||||
@@ -64,6 +79,10 @@ module Authentication
|
||||
cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax }
|
||||
end
|
||||
|
||||
def set_current_account
|
||||
Current.account = Account.first
|
||||
end
|
||||
|
||||
def terminate_session
|
||||
Current.session.destroy
|
||||
cookies.delete(:session_token)
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
module CurrentRequest
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action do
|
||||
Current.http_method = request.method
|
||||
Current.request_id = request.uuid
|
||||
Current.user_agent = request.user_agent
|
||||
Current.ip_address = request.ip
|
||||
Current.referrer = request.referrer
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,19 +0,0 @@
|
||||
class FirstRunsController < ApplicationController
|
||||
allow_unauthenticated_access
|
||||
|
||||
before_action :prevent_repeats
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def create
|
||||
user = FirstRun.create!(params.expect(user: [ :name, :email_address, :password ]))
|
||||
start_new_session_for user
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
private
|
||||
def prevent_repeats
|
||||
redirect_to root_path if Account.any?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
class Prompts::CardsController < ApplicationController
|
||||
MAX_RESULTS = 10
|
||||
|
||||
def index
|
||||
@cards = if filter_param.present?
|
||||
prepending_exact_matches_by_id(published_cards.mentioning(params[:filter]))
|
||||
else
|
||||
@cards = published_cards.latest
|
||||
end
|
||||
|
||||
if stale? etag: @cards
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def filter_param
|
||||
params[:filter]
|
||||
end
|
||||
|
||||
def published_cards
|
||||
Current.user.accessible_cards.published.limit(MAX_RESULTS)
|
||||
end
|
||||
|
||||
def prepending_exact_matches_by_id(cards)
|
||||
if card_by_id = Current.user.accessible_cards.find_by_id(params[:filter])
|
||||
[ card_by_id ] + cards
|
||||
else
|
||||
cards
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
class Prompts::Collections::UsersController < ApplicationController
|
||||
include CollectionScoped
|
||||
|
||||
def index
|
||||
@users = @collection.users
|
||||
|
||||
if stale? etag: @users
|
||||
render layout: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
class Sessions::LaunchpadController < ApplicationController
|
||||
require_unauthenticated_access
|
||||
|
||||
before_action :store_sig, only: :show
|
||||
before_action :restore_and_clear_sig, only: :update
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
if user = Current.account.signal_account.authenticate(sig: @sig).try(:peer)
|
||||
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 store_sig
|
||||
cookies[:_fizzy_launchpad_sig] = params.expect(:sig)
|
||||
end
|
||||
|
||||
def restore_and_clear_sig
|
||||
@sig = cookies.delete :_fizzy_launchpad_sig
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
class Signup::AccountsController < Signup::BaseController
|
||||
before_action :reset_signup_storage
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_signup_account_path, alert: "Try again later." }
|
||||
|
||||
def new
|
||||
@signup = Signup.new
|
||||
end
|
||||
|
||||
def create
|
||||
@signup = Signup.new(signup_params)
|
||||
|
||||
if @signup.recognized?
|
||||
store_signup
|
||||
redirect_to Launchpad.authentication_url(purpose: "signup", login_hint: @signup.email_address, redirect_uri: signup_session_url), allow_other_host: true
|
||||
elsif @signup.process
|
||||
redirect_to_account(@signup.account)
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signup_params
|
||||
params.require(:signup).permit(*Signup::PERMITTED_KEYS)
|
||||
end
|
||||
|
||||
def store_signup
|
||||
@signup.to_h.each { |key, value| signup_storage[key.to_s] = value }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
class Signup::BaseController < ApplicationController
|
||||
require_untenanted_access
|
||||
before_action :redirect_if_local_auth
|
||||
|
||||
http_basic_authenticate_with(
|
||||
name: Rails.application.credentials.dig(:account_signup_http_basic_auth, :name),
|
||||
password: Rails.application.credentials.dig(:account_signup_http_basic_auth, :password)
|
||||
)
|
||||
|
||||
attr_reader :authenticated_identity
|
||||
|
||||
private
|
||||
def signup_storage
|
||||
session[:signup] ||= {}
|
||||
end
|
||||
|
||||
def reset_signup_storage
|
||||
session.delete(:signup)
|
||||
end
|
||||
|
||||
def authenticated_identity=(identity)
|
||||
@authenticated_identity = identity
|
||||
signup_storage["identity_id"] = identity.try(:id)
|
||||
end
|
||||
|
||||
def set_authenticated_identity
|
||||
if identity_id = signup_storage["identity_id"]
|
||||
@authenticated_identity = SignalId::Identity.find_by(id: identity_id)
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_to_account(account)
|
||||
redirect_to account.signal_account.owner.remote_login_url(proceed_to: root_path),
|
||||
allow_other_host: true
|
||||
end
|
||||
|
||||
def redirect_if_local_auth
|
||||
render plain: "Unauthorized", status: :unauthorized if Rails.application.config.x.local_authentication
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,23 @@
|
||||
class Signup::CompletionsController < Signup::BaseController
|
||||
before_action :set_authenticated_identity, only: :create
|
||||
|
||||
def new
|
||||
end
|
||||
|
||||
def create
|
||||
@signup = Signup.new(signup_params)
|
||||
@signup.signal_identity = authenticated_identity
|
||||
|
||||
if @signup.process
|
||||
reset_signup_storage
|
||||
redirect_to_account(@signup.account)
|
||||
else
|
||||
render plain: "Could not complete signup. This is probably a bug.", status: :unprocessable_entity, layout: false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def signup_params
|
||||
signup_storage.slice(*Signup::PERMITTED_KEYS.map(&:to_s))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Signup::SessionsController < Signup::BaseController
|
||||
def create
|
||||
if self.authenticated_identity = SignalId::Identity.authenticate(params.permit(:sig))
|
||||
redirect_to new_signup_completion_url
|
||||
else
|
||||
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -35,7 +35,7 @@ module NotificationsHelper
|
||||
button_to read_notification_path(notification),
|
||||
class: "card__notification-unread-indicator btn btn--circle borderless",
|
||||
title: "Mark as read",
|
||||
data: { turbo_frame: "_top" } do
|
||||
data: { action: "form#submit:stop", controller: "form", form_target: "submit", turbo_frame: "_top" } do
|
||||
concat(icon_tag("remove-med"))
|
||||
concat(tag.span("Mark as read", class: "for-screen-reader"))
|
||||
end
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module RichTextHelper
|
||||
def mentions_prompt(collection)
|
||||
content_tag "lexical-prompt", "", trigger: "@", src: prompts_collection_users_path(collection), name: "mention"
|
||||
end
|
||||
|
||||
def cards_prompt
|
||||
content_tag "lexical-prompt", "", trigger: "#", src: prompts_cards_path, name: "card", "insert-editable-text": true, "remote-filtering": true
|
||||
end
|
||||
end
|
||||
+10
-1
@@ -1,5 +1,14 @@
|
||||
class Account < ApplicationRecord
|
||||
include Entropic, Joinable
|
||||
include Entropic, Joinable, SignalAccount
|
||||
|
||||
has_many_attached :uploads
|
||||
|
||||
def setup_basic_template
|
||||
user = User.first
|
||||
|
||||
Closure::Reason.create_defaults
|
||||
Collection.create!(name: "Cards", creator: user, all_access: true)
|
||||
workflow = Workflow.create!(name: "Basic")
|
||||
Collection.first.update!(workflow: workflow)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
module Account::SignalAccount
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
unless Rails.application.config.x.local_authentication
|
||||
# TODO: remove the "optional: true" once we've populated the accounts properly
|
||||
belongs_to :signal_account, class_name: "SignalId::Account", primary_key: :queenbee_id, foreign_key: :queenbee_id, optional: true
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def create_with_admin_user(queenbee_id:)
|
||||
new(queenbee_id: queenbee_id).tap do |account|
|
||||
SignalId::Database.on_master do
|
||||
account.name = account.signal_account.name
|
||||
account.save!
|
||||
|
||||
User.create!(
|
||||
name: account.signal_account.owner.name,
|
||||
email_address: account.signal_account.owner.email_address,
|
||||
signal_user_id: account.signal_account.owner.id,
|
||||
role: "admin",
|
||||
password: SecureRandom.hex(36) # TODO: remove password column?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,6 +19,10 @@ module Mentions
|
||||
|
||||
private
|
||||
def scan_mentionees
|
||||
mentionees_from_plain_text | mentionees_from_rich_text
|
||||
end
|
||||
|
||||
def mentionees_from_plain_text
|
||||
scan_mentioned_handles.filter_map do |mention|
|
||||
mentionable_users.find { |user| user.mentionable_handles.include?(mention) }
|
||||
end
|
||||
@@ -28,6 +32,14 @@ module Mentions
|
||||
mentionable_content.scan(/(?<!\w)@(\w+)/).flatten.uniq(&:downcase)
|
||||
end
|
||||
|
||||
def mentionees_from_rich_text
|
||||
mentionees_from_attachments & mentionable_users
|
||||
end
|
||||
|
||||
def mentionees_from_attachments
|
||||
rich_text_associations.flat_map { send(it.name)&.body&.attachments&.collect { it.attachable } }.compact
|
||||
end
|
||||
|
||||
def mentionable_users
|
||||
collection.users
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class Current < ActiveSupport::CurrentAttributes
|
||||
attribute :session
|
||||
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
|
||||
attribute :account
|
||||
|
||||
delegate :user, to: :session, allow_nil: true
|
||||
end
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
class FirstRun
|
||||
def self.create!(user_attributes)
|
||||
user = User.member.create!(user_attributes)
|
||||
|
||||
Account.create!(name: "Fizzy")
|
||||
Closure::Reason.create_defaults
|
||||
Collection.create!(name: "Cards", creator: user, all_access: true)
|
||||
|
||||
workflow = Workflow.create!(name: "Basic")
|
||||
Collection.first.update!(workflow: workflow)
|
||||
user
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
# lifted from bc3 app/models/launchpad.rb
|
||||
module Launchpad
|
||||
extend self
|
||||
|
||||
delegate :url, to: "SignalId.launchpad"
|
||||
|
||||
def login_url(product: false, account: nil, **params)
|
||||
url product_account_path("/signin", product: product, account: account), params
|
||||
end
|
||||
|
||||
def authentication_url(**params)
|
||||
url "/authenticate", params.merge(product: :fizzy)
|
||||
end
|
||||
|
||||
def product_account_path(path = nil, product: false, account: nil)
|
||||
product_path = "/fizzy" if product || account
|
||||
account_path = "/#{account.signal_account.id}" if account
|
||||
[ product_path, account_path, path ].compact.join
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,159 @@
|
||||
class Signup
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
include ActiveModel::Validations
|
||||
|
||||
PERMITTED_KEYS = %i[ full_name email_address password company_name ]
|
||||
|
||||
# Input attribute
|
||||
attr_accessor :company_name
|
||||
|
||||
# Input attributes when not using an existing identity
|
||||
attr_accessor :full_name, :email_address, :password
|
||||
|
||||
# Input attribute when using an existing identity, output when creating new identity
|
||||
attr_accessor :signal_identity
|
||||
|
||||
# Output attributes
|
||||
attr_reader :queenbee_account, :signal_account, :account, :user
|
||||
|
||||
validate :validate_new_identity
|
||||
|
||||
def initialize(...)
|
||||
@full_name = nil
|
||||
@email_address = nil
|
||||
@password = nil
|
||||
@company_name = nil
|
||||
@signal_identity = nil
|
||||
@queenbee_account = nil
|
||||
@account = nil
|
||||
@user = nil
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def process(destroy_existing_tenant: false)
|
||||
return false unless valid?
|
||||
|
||||
create_signal_identity
|
||||
create_queenbee_account
|
||||
create_tenant(destroy_existing_tenant:)
|
||||
|
||||
true
|
||||
rescue => error
|
||||
destroy_tenant
|
||||
destroy_queenbee_account
|
||||
destroy_signal_identity
|
||||
|
||||
errors.add(:base, "An error occurred during signup: #{error.message}")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def recognized?
|
||||
SignalId::Identity.find_by_email_address(email_address).present?
|
||||
end
|
||||
|
||||
def tenant_name
|
||||
@tenant_name ||= signal_account.subdomain
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
full_name: full_name,
|
||||
email_address: email_address,
|
||||
company_name: company_name
|
||||
}.compact_blank
|
||||
end
|
||||
|
||||
private
|
||||
def create_signal_identity
|
||||
unless signal_identity.present?
|
||||
SignalId::Database.on_master do
|
||||
@signal_identity = build_new_identity.tap(&:save!)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_queenbee_account
|
||||
@queenbee_account = Queenbee::Remote::Account.create!(queenbee_account_attributes)
|
||||
SignalId::Database.on_master do
|
||||
@signal_account = SignalId::Account.find_by(queenbee_id: @queenbee_account.id)
|
||||
end
|
||||
end
|
||||
|
||||
def create_tenant(destroy_existing_tenant: false, &block)
|
||||
ApplicationRecord.destroy_tenant(tenant_name) if destroy_existing_tenant
|
||||
ApplicationRecord.create_tenant(tenant_name) do
|
||||
@account = Account.create_with_admin_user(queenbee_id: queenbee_account.id)
|
||||
@account.setup_basic_template
|
||||
@user = User.first
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_tenant
|
||||
if queenbee_account && ApplicationRecord.tenant_exist?(tenant_name)
|
||||
ApplicationRecord.destroy_tenant(tenant_name)
|
||||
@account = nil
|
||||
@user = nil
|
||||
end
|
||||
end
|
||||
|
||||
def destroy_queenbee_account
|
||||
queenbee_account&.cancel
|
||||
@queenbee_account = nil
|
||||
end
|
||||
|
||||
def destroy_signal_identity
|
||||
SignalId::Database.on_master do
|
||||
signal_identity&.destroy
|
||||
end
|
||||
@signal_identity = nil
|
||||
end
|
||||
|
||||
def validate_new_identity
|
||||
unless signal_identity.present?
|
||||
build_new_identity.tap do |identity|
|
||||
unless identity.valid?
|
||||
identity.errors.each { |error| errors.add(error.attribute, error.message) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_new_identity
|
||||
SignalId::Identity.new do |identity|
|
||||
identity.name = full_name || email_address
|
||||
identity.email_address = email_address
|
||||
identity.username = email_address
|
||||
identity.password = password
|
||||
end
|
||||
end
|
||||
|
||||
def queenbee_account_attributes
|
||||
{
|
||||
skip_remote: true, # Fizzy creates its own local account
|
||||
product_name: "fizzy",
|
||||
name: queenbee_account_name,
|
||||
owner_identity_id: signal_identity.id,
|
||||
trial: false,
|
||||
subscription: subscription_attributes,
|
||||
remote_request: request_attributes
|
||||
}
|
||||
end
|
||||
|
||||
def subscription_attributes
|
||||
subscription = FreeV1Subscription
|
||||
{ name: subscription.to_param, price: subscription.price }
|
||||
end
|
||||
|
||||
def request_attributes
|
||||
{ remote_address: Current.ip_address, user_agent: Current.user_agent, referrer: Current.referrer }
|
||||
end
|
||||
|
||||
def queenbee_account_name
|
||||
name = company_name.presence || signal_identity.name
|
||||
name += " (Beta)" if Rails.env.beta?
|
||||
name
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
class Subscription < Queenbee::Subscription
|
||||
SHORT_NAMES = %w[ FreeV1 ]
|
||||
end
|
||||
|
||||
class FreeV1Subscription < Subscription
|
||||
property :proper_name, "Free Subscription"
|
||||
property :price, 0
|
||||
property :frequency, "yearly"
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class User < ApplicationRecord
|
||||
include Accessor, Assignee, Mentionable, Named, Role, Transferable
|
||||
include Accessor, ActionText::Attachable, Assignee, Mentionable, Named, Role, SignalUser, Transferable
|
||||
include Timelined # Depends on Accessor
|
||||
|
||||
has_one_attached :avatar
|
||||
|
||||
@@ -3,6 +3,11 @@ module User::Mentionable
|
||||
|
||||
included do
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :mentionee
|
||||
|
||||
# Need to set in the included block so that it overrides Action Text's
|
||||
def to_attachable_partial_path
|
||||
"users/attachable"
|
||||
end
|
||||
end
|
||||
|
||||
def mentioned_by(mentioner, at:)
|
||||
@@ -13,6 +18,10 @@ module User::Mentionable
|
||||
[ initials, first_name, first_name_with_last_name_initial ].collect(&:downcase)
|
||||
end
|
||||
|
||||
def content_type
|
||||
"application/vnd.actiontext.mention"
|
||||
end
|
||||
|
||||
private
|
||||
def first_name_with_last_name_initial
|
||||
"#{first_name}#{last_name&.first}"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module User::SignalUser
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
unless Rails.application.config.x.local_authentication
|
||||
belongs_to :signal_user, dependent: :destroy, class_name: "SignalId::User", optional: true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="flex flex-column full-width popup__list margin-none unpad max-width" data-filter-target="list">
|
||||
<ul class="popup__list" data-filter-target="list">
|
||||
<% @collection.users.active.alphabetically.each do |user| %>
|
||||
<li class="popup__group flex align-center full-width" data-value="<%= user.name.downcase %>" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= button_to card_assignments_path(@card, params: { assignee_id: user.id }), method: :post,
|
||||
|
||||
@@ -7,10 +7,13 @@
|
||||
<div class="comment__body rich-text-content txt-align-start">
|
||||
<%= form_with model: Comment.new, url: card_comments_path(card), class: "flex flex-column gap full-width",
|
||||
data: { controller: "form local-save",
|
||||
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| %>
|
||||
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: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } %>
|
||||
data: { local_save_target: "input", action: "actiontext:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
|
||||
<%= mentions_prompt(@card.collection) %>
|
||||
<%= cards_prompt %>
|
||||
<% end %>
|
||||
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
|
||||
<span>Post this comment</span>
|
||||
<% end %>
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
<div class="comment__body rich-text-content txt-align-start">
|
||||
<%= form_with model: [ @card, @comment ], class: "flex flex-column gap full-width",
|
||||
data: { controller: "form", action: "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, autofocus: true, placeholder: new_comment_placeholder(@card) %>
|
||||
<%= form.rich_textarea :body, required: true, autofocus: true, placeholder: new_comment_placeholder(@card) do %>
|
||||
<%= mentions_prompt(@card.collection) %>
|
||||
<%= cards_prompt %>
|
||||
<% end %>
|
||||
<div class="flex gap-half justify-start">
|
||||
<%= form.button class: "btn btn--reversed", type: :submit do %>
|
||||
<span>Save changes</span>
|
||||
@@ -18,7 +21,7 @@
|
||||
<% end %>
|
||||
|
||||
<%= tag.button type: :submit, class: "btn btn--negative flex-item-justify-end", form: dom_id(@comment, :delete_form),
|
||||
data: { turbo_confirm: "Are you sure you want to delete this comment?" } do %>
|
||||
data: { turbo_confirm: "Are you sure you want to delete this comment?" } do %>
|
||||
<%= icon_tag "trash" %>
|
||||
<span class="for-screen-reader">Delete</span>
|
||||
<% end %>
|
||||
|
||||
@@ -20,7 +20,10 @@
|
||||
<div class="card__description rich-text-content margin-block-start-half margin-block-end">
|
||||
<%= form.rich_textarea :description, class: "card-field__description",
|
||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
||||
data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
data: { action: "actiontext:change->auto-save#change focusout->auto-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do %>
|
||||
<%= mentions_prompt(card.collection) %>
|
||||
<%= cards_prompt %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= render "cards/display/preview/collection", card: card %>
|
||||
<%= render "cards/display/preview/tags", card: card %>
|
||||
|
||||
<% if card.staged? %>
|
||||
<% if card.staged? && card.doing? %>
|
||||
<span class="btn justify-start workflow-stage workflow-stage--current txt-uppercase min-width max-width">
|
||||
<span class="overflow-ellipsis "><%= card.stage.name %></span>
|
||||
</span>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="flex flex-column full-width popup__list margin-block-start-half margin-none-inline margin-none-block-end unpad max-width" data-filter-target="list">
|
||||
<ul class="popup__list margin-block-start-half margin-none-block-end" data-filter-target="list">
|
||||
<% Current.user.collections.alphabetically.each do |collection| %>
|
||||
<li class="popup__group flex align-center full-width" data-value="<%= collection.name.downcase %>" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= button_to card_collection_path(card, params: { collection_id: collection.id }), method: :patch,
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
<%= form.rich_textarea :description, class: "card-field__description rich-text-content",
|
||||
placeholder: "Add some notes, context, pictures, or video about this…",
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } %>
|
||||
data: { action: "keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do %>
|
||||
<%= mentions_prompt(@card.collection) %>
|
||||
<%= cards_prompt %>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= form.button "Save changes", type: :submit, class: "btn btn--reversed", style: "--btn-background: #{@card.color}" %>
|
||||
<%= link_to "Close editor and discard changes", collection_card_path(@card.collection, @card), data: { form_target: "cancel" }, hidden: true %>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
aria-label="Closed with reason…" aria-description="Closed with reason…"
|
||||
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half">Closed with reason…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:closure_reason_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column max-width popup__list full-width",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:collection_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -29,24 +29,24 @@
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<%= render "events/filter/header" %>
|
||||
<% if Current.user.collections.many? %>
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Collections</strong>
|
||||
<menu contents>
|
||||
<strong class="popup__group-title">Collections</strong>
|
||||
<ul class="popup__list">
|
||||
<%= render "cards/index/collections_filter", filter: filter %>
|
||||
</menu>
|
||||
</ul>
|
||||
<% end %>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Tags</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">Tags</strong>
|
||||
<ul class="popup__list">
|
||||
<%= render partial: "events/filter/tag", collection: Tag.all.alphabetically, as: :tag %>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">People</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">People</strong>
|
||||
<ul class="popup__list">
|
||||
<%= render partial: "events/filter/user", collection: User.active.alphabetically, as: :user %>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<% if platform.desktop? %>
|
||||
<footer class="popup__footer txt-small txt-align-center full-width">
|
||||
<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.
|
||||
</footer>
|
||||
<% end %>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
aria-label="In stage…" aria-description="In stage…"
|
||||
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half">In stage(s)…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:stage_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Collections</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column max-width popup__list full-width",
|
||||
<strong class="popup__group-title">Collections</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% if Current.user.collections.one? %>
|
||||
<%= link_to cards_path, class: "popup__group flex align-center", data: { value: Current.user.collections.first.name.downcase, filter_target: "item", navigable_list_target: "item" } do %>
|
||||
|
||||
@@ -25,22 +25,22 @@
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<%= render "events/filter/header" %>
|
||||
<% if Current.user.collections.many? %>
|
||||
<menu contents>
|
||||
<ul class="popup__list">
|
||||
<%= render "cards/show/collections" %>
|
||||
</menu>
|
||||
</ul>
|
||||
<% end %>
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Tags</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">Tags</strong>
|
||||
<div class="popup__list">
|
||||
<%= render partial: "events/filter/tag", collection: Tag.all.alphabetically, as: :tag %>
|
||||
</div>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">People</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">People</strong>
|
||||
<div class="popup__list">
|
||||
<%= render partial: "events/filter/user", collection: User.active.alphabetically, as: :user %>
|
||||
</div>
|
||||
|
||||
<% if platform.desktop? %>
|
||||
<footer class="popup__footer txt-small txt-align-center full-width">
|
||||
<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.
|
||||
</footer>
|
||||
<% end %>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
autocomplete: "off", data: { filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="flex flex-column full-width popup__list margin-none unpad max-width" data-filter-target="list">
|
||||
<ul class="popup__list" data-filter-target="list">
|
||||
<% @tags.each do |tag| %>
|
||||
<li class="popup__group flex align-center full-width" data-value="<%= tag.hashtag.downcase %>" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= button_to card_taggings_path(@card, params: { tag_title: tag.title }), method: :post,
|
||||
|
||||
@@ -25,30 +25,30 @@
|
||||
|
||||
<%= render "events/filter/header" %>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Collections</strong>
|
||||
<%= form_with url: events_path, method: :get, class: "flex flex-column max-width popup__list full-width",
|
||||
<strong class="popup__group-title">Collections</strong>
|
||||
<%= form_with url: events_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% if Current.user.collections.many? %>
|
||||
<menu contents>
|
||||
<ul class="popup__list">
|
||||
<%= render "events/filter/custom_collections" %>
|
||||
<%= render "events/filter/all_collections_option", form: form, filter: filter %>
|
||||
<%= render partial: "events/filter/collection_option", collection: Current.user.collections.alphabetically, as: :collection, locals: { form: form, filter: filter } %>
|
||||
</menu>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">Tags</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">Tags</strong>
|
||||
<div class="popup__list">
|
||||
<%= render partial: "events/filter/tag", collection: Tag.all.alphabetically, as: :tag %>
|
||||
</div>
|
||||
|
||||
<strong class="txt-uppercase txt-x-small margin-block-half pad-inline">People</strong>
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<strong class="popup__group-title">People</strong>
|
||||
<div class="popup__list">
|
||||
<%= render partial: "events/filter/user", collection: User.active.alphabetically, as: :user %>
|
||||
</div>
|
||||
|
||||
<% if platform.desktop? %>
|
||||
<footer class="popup__footer txt-small txt-align-center full-width">
|
||||
<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.
|
||||
</footer>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<div class="popup__list">
|
||||
<% @filters.each do |filter| %>
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to cards_path(**filter.as_params), class: "btn popup__new popup__item" do %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex flex-wrap align-center justify-center full-width max-width btn__group--chunky">
|
||||
<div class="flex flex-wrap align-center justify-center full-width max-width fizzy-menu__hotkeys">
|
||||
<%= link_to root_path, class: "btn popup__group borderless", data: { value: "home", filter_target: "item", navigable_list_target: "item", controller: "hotkey", action: "keydown.1@document->hotkey#click keydown.shift+1@document->hotkey#click" } do %>
|
||||
<%= icon_tag "home" %>
|
||||
<span>Home</span>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Assigned to…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:assignee_ids, :assignment_status).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
aria-label="Added by…" aria-description="Added by…"
|
||||
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half">Added by…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:creator_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
aria-label="Sort by…" aria-description="Sort by…"
|
||||
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half">Sort by…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:indexed_by).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
aria-label="In stage…" aria-description="In stage…"
|
||||
data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">In stage…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:stage_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<dialog class="events__popup popup panel flex-column align-start gap-half fill-white shadow" data-dialog-target="dialog" data-action="turbo:before-cache@document->dialog#close">
|
||||
<strong class="popup__title margin-block-start-half pad-inline-half overflow-ellipsis">Tagged…</strong>
|
||||
<%= form_with url: cards_path, method: :get, class: "flex flex-column full-width popup__list",
|
||||
<%= form_with url: cards_path, method: :get, class: "popup__list",
|
||||
data: { controller: "form" } do |form| %>
|
||||
<% filter.as_params.except(:tag_ids).each do |key, value| %>
|
||||
<%= filter_hidden_field_tag key, value %>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<% @page_title = "Set up Fizzy" %>
|
||||
|
||||
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 55ch;">
|
||||
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
|
||||
|
||||
<%= form_with model: User.new, url: first_run_path, class: "flex flex-column gap txt-large" do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.text_field :name, class: "input", autocomplete: "name", placeholder: "Name", autofocus: true, required: true, data: { "1p-ignore": true } %>
|
||||
<%= icon_tag "person", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Email address", required: true %>
|
||||
<%= icon_tag "email", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.password_field :password, class: "input", autocomplete: "new-password", placeholder: "Password", required: true, maxlength: 72 %>
|
||||
<%= icon_tag "password", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" id="log_in" class="btn btn--reversed center">
|
||||
<%= icon_tag "arrow-right" %>
|
||||
<span class="for-screen-reader">Create your account</span>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<lexical-prompt-item>
|
||||
<template type="menu">
|
||||
#<%= card.id %> <%= card.title %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
<%= link_to "##{card.id} #{card.title}", collection_card_url(card.collection, card) %>
|
||||
</template>
|
||||
</lexical-prompt-item>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/cards/card", collection: @cards %>
|
||||
@@ -0,0 +1,10 @@
|
||||
<lexical-prompt-item search="<%= "#{user.name} #{user.initials}" %>" sgid="<%= user.attachable_sgid %>">
|
||||
<template type="menu">
|
||||
<%= avatar_image_tag user %>
|
||||
<%= user.name %>
|
||||
</template>
|
||||
<template type="editor">
|
||||
<%= avatar_image_tag user %>
|
||||
<%= user.first_name %>
|
||||
</template>
|
||||
</lexical-prompt-item>
|
||||
@@ -0,0 +1 @@
|
||||
<%= render partial: "prompts/collections/users/user", collection: @users %>
|
||||
@@ -0,0 +1 @@
|
||||
<%= auto_submit_form_with method: :put %>
|
||||
@@ -0,0 +1,58 @@
|
||||
<% @page_title = "Sign up for Fizzy" %>
|
||||
|
||||
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 65ch;">
|
||||
<h1 class="txt-xx-large margin-block-end-double">Fizzy</h1>
|
||||
<h2 class="txt-large margin-block-end-double">Create your account</h2>
|
||||
|
||||
<%= form_with model: @signup, url: signup_accounts_path, scope: "signup", class: "flex flex-column gap txt-large", data: { turbo: false } do |form| %>
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.text_field :full_name, class: "input", autocomplete: "name", placeholder: "Your name", autofocus: true, required: true %>
|
||||
<%= icon_tag "person", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.email_field :email_address, class: "input", autocomplete: "username", placeholder: "Your email address", required: true %>
|
||||
<%= icon_tag "email", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.text_field :company_name, class: "input", autocomplete: "organization", placeholder: "Your organization's name", required: true %>
|
||||
<%= icon_tag "building", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex align-center gap">
|
||||
<label class="flex align-center gap input input--actor txt-large">
|
||||
<%= form.password_field :password, class: "input", autocomplete: "new-password", placeholder: "Password (at least 12 characters)", required: true, maxlength: 72, minlength: 12 %>
|
||||
<%= icon_tag "password", class: "txt-large" %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<% if @signup.errors.any? %>
|
||||
<div class="alert alert--error">
|
||||
<ul class="margin-block-none">
|
||||
<% @signup.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<button type="submit" class="btn btn--reversed center">
|
||||
<%= icon_tag "arrow-right" %>
|
||||
<span>Create your account</span>
|
||||
</button>
|
||||
<% end %>
|
||||
|
||||
<footer class="margin-block-start-double txt-center">
|
||||
<p class="txt-small txt-muted">
|
||||
Already have an account?
|
||||
<%= link_to "Sign in", Launchpad.login_url(product: true), class: "decorated" %>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<%= auto_submit_form_with url: signup_completions_path, data: { turbo: false } %>
|
||||
@@ -0,0 +1,2 @@
|
||||
<%= avatar_image_tag user %>
|
||||
<%= user.first_name %>
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
echo "Access with david@37signals.com / secret123456 on http://37signals.fizzy.localhost:3006"
|
||||
echo "Access with david@37signals.com / secret123456 on http://honcho.fizzy.localhost:3006"
|
||||
echo "Access first run on http://first-run.fizzy.localhost:3006"
|
||||
|
||||
if [ -f tmp/solid-queue.txt ]; then
|
||||
# see also config/environments/development.rb
|
||||
export SOLID_QUEUE_IN_PUMA=1
|
||||
fi
|
||||
|
||||
|
||||
@@ -7,8 +7,13 @@ system("gem install bundler --conservative")
|
||||
system("bundle check") || system!("bundle install")
|
||||
|
||||
puts "\n== Preparing database =="
|
||||
system! "bin/rails db:prepare"
|
||||
system! "bin/rails db:reset" if ARGV.include?("--reset")
|
||||
if ARGV.include?("--reset")
|
||||
system! "bin/rails db:drop db:create db:schema:load db:prepare"
|
||||
system! "bin/rails db:fixtures:load" # load signal id fixtures before running fizzy seeds
|
||||
system! "bin/rails db:seed"
|
||||
else
|
||||
system! "bin/rails db:prepare"
|
||||
end
|
||||
|
||||
puts "\n== Removing old logs and tempfiles =="
|
||||
system! "bin/rails log:clear tmp:clear"
|
||||
|
||||
@@ -10,6 +10,7 @@ CI.run do
|
||||
step "Security: Brakeman code analysis", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
||||
|
||||
step "Tests: Rails", "bin/rails test"
|
||||
step "Tests: 37id", "bin/rails 37id:test:units"
|
||||
|
||||
if success?
|
||||
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
||||
|
||||
@@ -1 +1 @@
|
||||
/M5cK3B/Wq1R2f4XsQQ8t7qJGCNMaBU3itDDKNZapeGMjrlE4A9CKOh2RPQsKlrsGjJdgGL4YdfDiT1LiF0pD/KL/t5Fw8Z8GMndzA+hmpvQ+hgufvd8n2ZzK/SIK6RmfC9BabdlvuQ0rr3/EdQJ5yIgk3WtWv73bOWDopqHerEAcaNl8KjCu5n2vokAidqEh0/O30yQQB9PAFMRqEwapLVcDSnaJXr6OQnAAm9tIoWaEaIF8ZJH3Rmn0COs8xZqYFyFv/pP3gVy3Mw+VL3aIkEf/eZ3IQWYNf7fiKPvIdNVsmghmeoQRqCp4E0dpn2XDSEGx5i5oq7ynSDCPky4GahiLtpe3vas+Z+lzD5hAAZmlX0G615b4hWjAViTEQU+CMXhmpj7eVpT9co+1oKmqtbDvHr7uCJIf4d17kC13u/8yjgl2SaCjRWx94wh8su/h6h9Xw0qewITbfM32pFRiH+R5bc=--fxg4Lf+qmYeuvyZp--qVZ+ELV0FRzBb69MUAqOsQ==
|
||||
yD+Wb/DYMXUXf16brcYmNb7OZngx7PuA6gLP8w6ZV7pA/GFweTIXQw67YoXebL6A7gWxhXX4tvpnrz9xPC1p5+5a8q00lOhcybc/480bSfenIXExDpg6mSDbQLRomrpwZsr+IqBWRC8WoXTtpVYRybEPRP88ZXdyE0MjnfOlGcr1fUcF3r9T8biXfNhS9v9UTxck1Q2yzGoNFRvBm3KgBN2QlJocG5AXaDYUSMfLoLmtkicO0KVOD7eYBxe0kDevUK7ZQGxbtATGIsjAJEifhuaPTj/Q2DH134TU4rlPr6n6vm9YEgC/CGVoqPIDgvovVd5IiqZlxGuVDDw3ct0cPH4/AnDLOVHiubi5/pZRCQyeOF+G1OmLVFUVeuZ6alPVrJSxHFxGVM/HT+pLWiReR/IJYacLzb3b2OOtBp/AKxbt25YxFFHuFHABfVEqPRo0qVwbQaxKZEyDyEW0t0ktTkqHywqkbmsx3HPU3skV42rTHf9c1udJhzTXZJPKIWKP96ahgX6eaOhy09rzwD99RnKLJuAuMl80Cw1L+ftLG5hM4tHYmv+JbpWDSInvP1jtA5iWtDRSiqhTxvXw4ItHximvsERI5k6hSPJzrzqnhLUxNVi10KQHh5KGq7EiC2J3HJwioY+fWVivzEJdw2WuVFsovcFyolrT3tyqcMVF5V3dQw4/gOGQ7gXsz0a9sb5wQgXJ9pyZBAmD4Bg7r4wD0f0JiL/5EdKOkAOTV2JDt0OgoSWzU+yuvByuWxPn2quoX0ZSG0huHvl2NK33RoR9aXLcSjtmi5XJGXpjATfKMoA2d8JBIDiJcOPAPwWkpI7AOnmvlV2SmrqPlLTkO0MfdJ528k8CFi2puSRQN+CEPhyhZclDO/J2IP94qmE35xAmWM7yjY6E+EZY9uhlF39uBhAIXylZwTyF31PSCqwpQJthH62pDFWpCgjy1iax482cCVt5n2HLQpmSNaPAvsI7hZAGFJsrDncwfJiK68YXrgmTeg8=--Gd2i+KrusH11oais--SraJ0tcCfEgIdRSJYoCoNw==
|
||||
@@ -1 +1 @@
|
||||
e6HcyVNJvgdJGTCcvW8FDHQ9pHYmTgNW2/71Espdwxd5LQX/Xb9+VkeqpEAL8ceFJo6ro49mMs85G2YngKbbXEh//8+XSS0inMR/kh9wTV3f6YBWnLVBbhrOnYgPcQ8vedsgxLG2Cs4y7ipfeT/dp9bb5m4IxV0OPiUjVFFJuAuGkMJt3pE7D6alqL1ACRxazV9UKV5WKEBfaQYeKpJ++5f86Nls1EwKo5tB1lJed9P5n2yH7meX/QyxjgQd38CIaPwp+F92y5DLKJD5MZRMJGgWf6MT6096LhjEC3vvtS3wwT/eDVcVKTplRkEszj7BZEfOs9Ondb9EVtbXmeoHYSgpaOq9/nEwtho5Uw==--KlHZy1apg3u1XxFs--N2ehRobLHNz+Sepc8p4EIA==
|
||||
hHi9VfqfZI57GYR5r75Rh00L3FYeoUgsdtCgBzJGdPhhGZLGlCgUAcY6tgVhqhgM/u5WxyPAzmoV+bbQKb0wO0U6KUmrDKDv9uRnNSUjXGYsg8gWdTUTfx7WBFOFawV3p1Dsm7zM1oUJbRWgy2TiMFl5kP9CN4Lk78n5a1/Y7mnx/wmzwUyQv+BUFJ0UKCfRmWieaNoLkm5kWwyepWHaSRU5EVda6J9VtuwP3hISIzncXm0jpr9r/tJm8flgUR3o2STIVHVgg+YcWz0j3st5RMv7i3D2OHfNGx84vsf3LNlsiLGB1ZU0IsmchLOSqPmlDK2oiB7UKepTPthGUFzNmcDG4LpUQfNwn7B8P7by0zadVefdLSB3kFCNLX+KCAbcjHPlWufkagwHo+dV+iIVACC+hfWl3L/IouqYS0ln2XCBDz6A8lI8PHeGaJe9UhfA5JEuJLRPui8znoq7LrKo4/oBB7ArKdHz/W/oHJA9+aLHcLOq9TPSExAsGi1wcjdgF0nk5mB9iX+az3//1zM64rRz8EyzNkBg1I6PV6wpzROLubttv1+I5nRVe4BernNfVJPi/hbvrlrcZmouU32oOzd2aZuPL5/H8gBhMOPlN25Rr51XCziaczgdEfOwTO8da6r7pxsJDkrvKi+Ju6efxhZ6GeotWJuEUtQtWhHnzvhcJAXfUtJf6A==--LWtZauLkWq+tD+4L--mkMcqYebO9TDi6RTRNUQvA==
|
||||
@@ -1 +1 @@
|
||||
Qkkt3krcGlOh76//7TmY5rXz31mhL3NNNiO6TCcRhx4lT6uZ8sLjW7X0yp2wZ30y2cJBeIsNMeOhL47JTpu0Z4d0FPtwgH4bzzXAiab0/YrfMjxyBt7/K1DzW/bePUvakJggIvVGGRA1NB1tPkwziQ4dgA34E/UUPAxUofFaoCIFVABmqt4DFg0DKYB8tImDDjZ5xUfSNxrKj3pH5mOIU9jqSK6LtBELFZcNxrJOI0UEgayRNwYAs44ujdM2/2zrAWlUCFZ3NnMU4bBJW02/d6LaxHtB809u8mx3bChEzWMtZB9CvpbRH7u6XfZJR1YGFztcNguzRbNX5x9pKZImAuAhVFKR7Fbi12Hg3w7U3A/YovwmyhHK6ksfOzYlYoDUTBnGYVwiqR16naaHCMekomK+uo0qtOn83RQkLuuqFJshPXn/lW8Rggt4dq/0THEl7Qj+dumadlWDVEgxhipI031RSBF1toNWw/zwaC31Ord1YXD8ZC09sPoj8Zd4hD2o2a0IUuIMXA8w5/2+AmdU8wF66opdKGdsZDRjM2Bul8aAbJ7/iV7xTuvlE4LmzXGArMvivIZ0GwrN3YAsDta2Y3ufibq7WZzSCOrpbPKJ9CEGg4a2AN8UGaGZ5ZEAYg8lhDgsZMjHRkGNuxk8QkDiVFDyNoI7H9ZQqbw+L1AJRTfDIcNB/9h5n+//Nk4ylLF1lnI7y/IQA/AuSQ+UN+okt3RhLugwGvWrs49MExJStLHpGveSoQ==--MQ2JycR3X6LIiKSY--45C7bleKX9FMGv4fc+G1yw==
|
||||
cPaSSbJ2XWIjNXDRi25iIH41V4zR1fKVTe3OV1XwZKsDE3950DL9Sk0RI/1bxYY+agT3x88vrsqm8xyqLwqcMeMg7J4el52WI4hORe5XLGJTcvs8CTgguut1s7c5Jb5ihOSPX9ZYEKnONxLU2BFe951WZgO+7Us2IBFbCQiqo8PXEBEONePuE9Aw2z1fUFW7NvxhptN9ws7DptP2O/wpHP8+KJmVuAJl+/qJHJoBh770dwQU3yCFz9mERowkARK4Z16Q+HelkrkvW9UziV53tMIExDRvfMnEKJz4wrU/AqVSiQIMQA6eVfTMQLxbVhBhLAlcV0Qic7yEbXLcAXwLX6Kga6aMOqyJvxh+JUnMpQavtHKikVncN8EI1lGxd/+mddS+khHZmCqkRvWl+LiaOe2WOkNENYXlGYANeRSTDy2BqMeT1ymAj3YxfWKYdR34Ukmm2iZKQ1d6fF+Dg/IT8/W/rIZPajQjMTO05WqeoE8kZ4lhgI7AxZHg9apHGsfkq6iBaf4kZUBFiLgOv0CR4Xz3zWVUHEyPx5afkkxs1u9NCspOaYWT4gA1tekFmaH7cMyhaQ/SEjOEs5AWyh47PeMBvwUumJKIEeil6p6K9109nSFG+BPszfhshY8G3aonuIbiWoJQAiGGnbvw4oZeAPjl8XNBYTWRspl2T9ep3krPg9L4eTrAyXdT0dG7GYXSa5NkrV86HugU5rDfx87N4QJVu6qr7Myzw6kEWlLRXHR6HPg+H7qZenNwuOfgnD3YoFOEWQfLiz9U3MqDRJIq5j+i2OfL4pv0VVpc51z42mWM1hvazGNiiTZvK0DohBja6D9h5Gl6ZP/eOGB8O6ZxIwn6mp23BIvxA5jBRGslCj/Op7msnkSAP0vtuI0hnCtfsOx0RfAPvj1/ETjVkRFhM7heWTgfqGQbjSVGvd/Dj6j3vbqGz3RASVL48juoWx/pezUMnWGZqRTqx5tD6FrZQy1yf/2Uoc8yMJQU14raqiShEqNveJswMv1D9kBnT3551DtpAmrMT+iuCX5aZHwVnY/sI8J8HdMAqFfpipMbCbccI/PgTc+U8uOqc+BBKYy8u04wHLnGu5wREewU5xRIkxcwGm4M4LkTLiVEJFUQuFEhylmxkCIDYG+WSBZ5xy/0dRUeZs26Uuvvs2//bXqn75Ia86Fpv54AzfdDqC9s0B1r5/ZqXtFjjL3bQyhv+SP5TMurAZcI0rLYay+oFcLZAv31zoo0dFcufiCKYTjbYEIlO+HW2aeVeOhUnCalqqslt4ql5A==--tVMd1KheRgH8F0gZ--x8Wzmk8JsMuHPG3JMOODLA==
|
||||
@@ -1 +1 @@
|
||||
Tc514fYVlm1c2c81KUFmuAGZ9rSZT7XfeGUEdTEvJNDD6JE9IghKbAAGgc6AalIt2/VxzSV6AGvNagb0/qlJrMOFeSJLqzufKt1YoLRUZtSUZDtzmI/2WSzju0Tyu4za6rnLEu+WvDc0NM8KZEv/TwwUbSOwoEVDQmava/iuWyvFr5sOH1wNr2kwYbErwDpThKcYqacI4CdvgpgXc//3T0Z5vsVCs6d8723DymM0KkSK9so8WrcCesk2fmhM1K2hQV7QdckaVuOL0JEj1AI7KzdJm7REoeLwRwaBeVDkqDNo2fKxwZ2XmU2ViJOozPsowx+324wQQZjbsf/0ohtDkgjbfMmA2tjkuNJdJFwIIc5VyvILhVs5WQIJHSXmabhUKxzKRCRZNBSaadLGCb7/AGfULKvh2vP1BWqm80EkDmhHm42ucJW1vA377y9UYWVcdvqWtp5m2F7HlXqi9Uw/+kFV2X6Jw1c07FsCu35TrUb5u8G7mu/866GBgf5VlfDVs1BA3aru05CtcsXi2qKMw0II7fZps/4XpYCB+VehXAyLi2ABVeTnqSfF7HwasXvfD3Jgh7zJLD0hudNDpu6xkmoyeksVBn034dmQf0TwAIKmNjEKMDxgAG50T2JhnEvHZEnqzGl6O8if8DKM5rPgt3XEU3zN/U3yA8T0eTyFdGlWbYK7Ergu4TcGo3WPk6bJhTGOHQ9puD89t6JGHbKDYW77w44SHv+C42flmNkzcndXlpvZvnEafDYeW6ofRC4eeTRX4+WKL4CYhlel78+twD6Mh0MV42MYM2rmtl66NHN8fWxyfoyjrNcXveKVAsGMatB0+dOglq6GmOiXRAt/I6D+7QYZLyaUhYNmmiuZXF4=--H6KWPnHc9qUQE5AI--PN98cg9t9ZhxMUEax/Mfgg==
|
||||
UV9AzQwwjGoWFh78GyKkNewL4lmsL80XT+6ca0JIxZ632OMKbYzHbPtJAD6RK1gaEB4GRkiBE2mLEdk8nYI47p8K9NVqZuI/ykgl23Sr2JzBpnHfjtUYlPQC7nYyeAR3gYtASD5+jSXvP7z/9BquIVdTvih3hWPPujDK5OROXagDtlGTZ4SgKZwihCTZczTwZRLxx5UguwbtuowrtcuPTg+r/H+zdJyRUU9kF7vYqV5VrP0vj0mXZUf5hWNBpKvhDtqgKUJNU87ds7xTLwMuQT0MCASoJXS0JSpIKn653baXczQue6JyuFcOlqDPDmBWs2GLh6pq3XnNolmWoyNehD9WClmJ5cu0l1dPmYZ0WnOfPgJCPtuJI4onIUj2nLC5m0tHpQPNs9WjazQeCeiw7TJBESK3b3lB4/Q7STshfJd74IIJ0crzfm0a6MwxvT184etKpNRQkgJpwK6WRNPzzzHdlTmbkEher4ilcs3QrmyLPYaLcuG+0YyDcjgJXRvLumkZilONbuBHluMc64u4GAU8QyW1dp31DrCw/HTOTtPEsRJSKdTIiFFCDpYt0Uq16Qbp9Hsg1ruPqFPELXDqRSl+N9ixDdXjF8WyQc6Gx4i8Fsur0WBLGBEtCEkCCQvwsH+TVH47S68CSiUui0c2zX1sM2SEcOOL7Cn0TUDWg6d8MgHQ5977aGLGFUkeuldv8KfQSra5uHKESSxOwn/eJJQMERSiVHSEj1EK3v4R+shXnviPsFTr3uERjdn2s1CcSk3qJ7J5WFpmgTL3/qKRWDroI96QO24CSr8JWJ3MzrwkLLpiEf6qLyfswiRjJ79sxk1pTdx7zedb8MuG/596J/xFI7GdbOI6qr2NvVUHH6r6PqXltMBoT/JU4pO7QOXfuok8zYGOj2gMzktCOdvEoydh+8QrsKN6W3mpg8oweQoq/2tgRm/YTeJMz+MQMR+J+aid2lSu8/wiHtsv8WdxJ7qxI7G3DslzbTEz0e3FAP+CEFOVgwN+nitWZpSyst6WiupzU5Ou9NxYiWbmhy5nhDJFC+MiLs8bS2kWfq3z1/Wsuyp8hBn/T0+MYtQ/cRnAxTE0sUZMB5xpffuGPlx3lgrmxV7wlPAzGooCGkGVUQQ29ljYMoGWqW34EAniIYHLUnBfdS+bNbui5rxVEAFyD4r57eqEaYYOKCFwkhNfYhHUeV+noDQ8uvCzppvRHGKJrkUCS0/A6z4KP8iW8wsETMBX/ShvFf6m44KNGR4IPzfn+r7uO9Pbukit3RJ8RsHZcC3RmH7/gtO3vW8G1BBrT5nI6BdRp9a9LdTx2qn32cKQD7Wc7S1KyFYn5reWSewlxNHHmgy4R/pEXTtdZJ5tuzweeYK/jr78CTrgEEN/btNawNh6SdTwRls5lu33LAQ=--3mgI5AzJFOeaM6Y1--KgIowEbtW/iHXiDrWGk2ug==
|
||||
@@ -1 +1 @@
|
||||
1QS4QGl05L2YvkEusirttYv2gIfRbNLEWTK7pOXG/HxqyWbWj9o5/GbTFeLI7K0sbcEjz+gcPcVKB+FGnkJECExynSvIN02bg5gP0QegTC3WA2vWInlr+gpoWCNgsazOgHKMxDBnQXc2myqhwDLKRsDHCQTW/5lIiKGBtBOLaml8KrJg84SeYCh54tJUjAp5yRkrhJQBHVgSSUfXRaFcrmdbPls5FDgB+rErACpp4BgYdXZoDds2RITDgQ==--kpoxQbOswL3cInhK--r+dB3hJHvjfUH1EBNzBLSg==
|
||||
0EWG0dpq/qELmTCqAgzEL2BZH7JCNFe6DDIynyKE06qiBCYhHs5+fMjmpzN0uwz43tqAAKJdhO/hZY4jXmTpxu+LSjwrUKAlxsWvSGu4vHEbJ5Fe+BzhnS84OFse6ma9akUAHaed3gt31wtrt2Y4AbSPedNvriqfYln7cvpgWbvjTgtEbHrcNMfjCBnaXXdO3n1zILunILgLiV7oBW0qurIrl2xOc4mbj4AqVAIElyh0md9fFtuBfsaRj2U9x/+7McbgTFODhHfeqoErEw7fc1RBFrNi2YNuHBSSqegQ6WtWbwq1Yg2Juv/Lw3g4IIdWadJsW3TdTMMiwmBMzOikx3lNX1Z399glvGNFVnJfBubKf6BxvFfAQ85PBzOZPl+/PTs=--GY44JMs5ZO1/eL6G--EhxEWuitiex6eHROpPQamw==
|
||||
@@ -13,6 +13,7 @@ proxy:
|
||||
env:
|
||||
clear:
|
||||
RAILS_ENV: beta
|
||||
LOCAL_AUTHENTICATION: t
|
||||
|
||||
x-beamer-accessory: &beamer-accessory
|
||||
image: basecamp/beamer
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Queenbee.host_app = Fizzy
|
||||
|
||||
Rails.application.config.to_prepare do
|
||||
Queenbee::Subscription.short_names = Subscription::SHORT_NAMES
|
||||
Queenbee::ApiToken.token = Rails.application.credentials.dig(:queenbee_api_token)
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
require "signal_id"
|
||||
|
||||
Rails.application.config.x.local_authentication = ENV["LOCAL_AUTHENTICATION"].present?
|
||||
|
||||
ENV["SIGNAL_ID_SECRET"] = Rails.application.credentials.signal_id_secret
|
||||
|
||||
Rails.application.config.to_prepare do
|
||||
SignalId.product = "fizzy"
|
||||
|
||||
db_config = SignalId::Database.default_configuration
|
||||
if Rails.application.config.x.local_authentication
|
||||
db_config.each do |name, config|
|
||||
config["connect_timeout"] = 1
|
||||
end
|
||||
end
|
||||
SignalId::Database.load_configuration db_config
|
||||
SignalId::Database.enable_rw_splitting!
|
||||
|
||||
silence_warnings do
|
||||
SignalId::Account::Peer = Account
|
||||
SignalId::User::Peer = User
|
||||
end
|
||||
end
|
||||
|
||||
Rails.application.config.after_initialize do
|
||||
ActiveRecord.yaml_column_permitted_classes << SignalId::PersonName
|
||||
end
|
||||
@@ -2,10 +2,17 @@ Rails.application.config.after_initialize do
|
||||
# in production and staging, we're using a two-level subdomain like "tenant.fizzy.37signals.com".
|
||||
# in development and beta it's only a single-level subdomain.
|
||||
Rails.application.config.active_record_tenanted.tenant_resolver = ->(request) do
|
||||
next nil if request.path == "/up"
|
||||
|
||||
tld_length = [ "37signals.com", "37signals-staging.com" ].include?(request.domain) ? 2 : 1
|
||||
request.subdomain(tld_length).presence || "return-404" # this is a quick fix for now
|
||||
|
||||
if request.path == "/up"
|
||||
nil
|
||||
elsif subdomain = request.subdomain(tld_length).presence
|
||||
subdomain
|
||||
elsif request.path =~ %r{^/(queenbee|signup)\b}
|
||||
nil
|
||||
else
|
||||
"return-404" # this is a quick fix for now
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+19
-2
@@ -1,6 +1,4 @@
|
||||
Rails.application.routes.draw do
|
||||
resource :first_run
|
||||
|
||||
resource :account do
|
||||
resource :join_code, module: :accounts
|
||||
|
||||
@@ -87,6 +85,14 @@ Rails.application.routes.draw do
|
||||
scope module: "sessions" do
|
||||
resources :transfers, only: %i[ show update ]
|
||||
end
|
||||
resource :launchpad, only: %i[ show update ], controller: "sessions/launchpad"
|
||||
end
|
||||
|
||||
namespace :signup do
|
||||
get "/" => "accounts#new"
|
||||
resources :accounts, only: %i[ new create ]
|
||||
get "/session" => "sessions#create" # redirect from Launchpad after mid-signup authentication
|
||||
resources :completions, only: %i[ new create ]
|
||||
end
|
||||
|
||||
resources :users do
|
||||
@@ -105,6 +111,15 @@ Rails.application.routes.draw do
|
||||
resources :pins
|
||||
end
|
||||
|
||||
namespace :prompts do
|
||||
resources :cards
|
||||
resources :collections do
|
||||
scope module: :collections do
|
||||
resources :users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :public do
|
||||
resources :collections do
|
||||
scope module: :collections do
|
||||
@@ -143,6 +158,8 @@ Rails.application.routes.draw do
|
||||
|
||||
root "events#index"
|
||||
|
||||
Queenbee.routes(self)
|
||||
|
||||
namespace :admin do
|
||||
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddQueenbeeIdToAccounts < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :accounts, :queenbee_id, :integer # I solemnly swear I will add a "NOT NULL" constraint once it's populated
|
||||
add_index :accounts, :queenbee_id, unique: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class AddSignalUserIdToUsers < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
add_column :users, :signal_user_id, :integer
|
||||
add_index :users, :signal_user_id, unique: true
|
||||
end
|
||||
end
|
||||
Generated
+6
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2025_06_12_163028) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "collection_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
@@ -26,7 +26,9 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "join_code"
|
||||
t.string "name", null: false
|
||||
t.integer "queenbee_id"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["queenbee_id"], name: "index_accounts_on_queenbee_id", unique: true
|
||||
end
|
||||
|
||||
create_table "action_text_rich_texts", force: :cascade do |t|
|
||||
@@ -324,6 +326,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
@@ -333,9 +336,11 @@ ActiveRecord::Schema[8.1].define(version: 2025_06_09_102553) do
|
||||
t.string "name", null: false
|
||||
t.string "password_digest"
|
||||
t.string "role", default: "member", null: false
|
||||
t.integer "signal_user_id"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["email_address"], name: "index_users_on_email_address", unique: true
|
||||
t.index ["role"], name: "index_users_on_role"
|
||||
t.index ["signal_user_id"], name: "index_users_on_signal_user_id", unique: true
|
||||
end
|
||||
|
||||
create_table "watches", force: :cascade do |t|
|
||||
|
||||
+71
-3
@@ -113,6 +113,16 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: queenbee_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
@@ -983,6 +993,16 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: signal_user_id
|
||||
cast_type: *1
|
||||
sql_type_metadata: *2
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
watches:
|
||||
- *22
|
||||
@@ -1163,7 +1183,23 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
accounts: []
|
||||
accounts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: accounts
|
||||
name: index_accounts_on_queenbee_id
|
||||
unique: true
|
||||
columns:
|
||||
- queenbee_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
action_text_rich_texts:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: action_text_rich_texts
|
||||
@@ -2200,7 +2236,23 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags: []
|
||||
tags:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: tags
|
||||
name: index_tags_on_account_id_and_title
|
||||
unique: true
|
||||
columns:
|
||||
- title
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: users
|
||||
@@ -2234,6 +2286,22 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: users
|
||||
name: index_users_on_signal_user_id
|
||||
unique: true
|
||||
columns:
|
||||
- signal_user_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
watches:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: watches
|
||||
@@ -2285,4 +2353,4 @@ indexes:
|
||||
comment:
|
||||
valid: true
|
||||
workflows: []
|
||||
version: 20250609102553
|
||||
version: 20250612163028
|
||||
|
||||
+38
-11
@@ -7,24 +7,52 @@ def seed_account(name)
|
||||
puts " #{elapsed.round(2)} sec"
|
||||
end
|
||||
|
||||
def create_tenant(name)
|
||||
ApplicationRecord.destroy_tenant name
|
||||
ApplicationRecord.create_tenant name
|
||||
ApplicationRecord.current_tenant = name
|
||||
def create_tenant(signal_account_name)
|
||||
signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name)
|
||||
tenant_name = signal_account.subdomain
|
||||
|
||||
ApplicationRecord.destroy_tenant tenant_name
|
||||
ApplicationRecord.create_tenant(tenant_name) do
|
||||
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
|
||||
account.setup_basic_template
|
||||
end
|
||||
|
||||
ApplicationRecord.current_tenant = tenant_name
|
||||
end
|
||||
|
||||
def create_first_run(name, email_address, password: "secret123456")
|
||||
FirstRun.create!(name:, email_address:, password:)
|
||||
def find_or_create_user(full_name, email_address)
|
||||
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.first.signal_account
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
|
||||
if user = User.find_by(signal_user_id: signal_user.id)
|
||||
user.password = "secret123456"
|
||||
user.save!
|
||||
user
|
||||
else
|
||||
User.create!(
|
||||
signal_user_id: signal_user.id,
|
||||
name: signal_identity.name,
|
||||
email_address: signal_identity.email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def login_as(user)
|
||||
Current.session = user.sessions.create
|
||||
end
|
||||
|
||||
def create_user(name, email_address, password: "secret123456")
|
||||
User.create!(name:, email_address:, password:)
|
||||
end
|
||||
|
||||
def create_collection(name, creator: Current.user, all_access: true, access_to: [])
|
||||
Collection.create!(name:, creator:, all_access:).tap { it.accesses.grant_to(access_to) }
|
||||
end
|
||||
@@ -36,4 +64,3 @@ end
|
||||
# Seed accounts
|
||||
seed_account "37signals"
|
||||
seed_account "honcho"
|
||||
seed_account "first-run"
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
create_tenant "37signals"
|
||||
|
||||
david = create_first_run "David Heinemeier Hansson", "david@37signals.com"
|
||||
david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com"
|
||||
jason = find_or_create_user "Jason Fried", "jason@37signals.com"
|
||||
jz = find_or_create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = find_or_create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
|
||||
login_as david
|
||||
|
||||
jz = create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
|
||||
create_collection("Fizzy", access_to: [ jz, kevin ]).tap do |fizzy|
|
||||
create_collection("Fizzy", access_to: [ jason, jz, kevin ]).tap do |fizzy|
|
||||
create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy)
|
||||
|
||||
create_card("Prepare sign-up page", description: "We need to do this before the launch.", collection: fizzy).tap do |card|
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
create_tenant "first-run"
|
||||
+9
-8
@@ -1,15 +1,16 @@
|
||||
create_tenant "honcho"
|
||||
create_tenant "Honcho"
|
||||
|
||||
david = find_or_create_user "David Heinemeier Hansson", "david@37signals.com"
|
||||
jason = find_or_create_user "Jason Fried", "jason@37signals.com"
|
||||
jz = find_or_create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = find_or_create_user "Kevin McConnell", "kevin@37signals.com"
|
||||
jorge = find_or_create_user "Jorge Manrubia", "jorge@37signals.com"
|
||||
mike = find_or_create_user "Mike Dalessio", "mike@37signals.com"
|
||||
|
||||
david = create_first_run "David Heinemeier Hansson", "david@37signals.com"
|
||||
login_as david
|
||||
|
||||
jason = create_user "Jason Zimdars", "jz@37signals.com"
|
||||
kevin = create_user "Kevin Mcconnell", "kevin@37signals.com"
|
||||
sarah = create_user "Sarah Johnson", "sarah@37signals.com"
|
||||
mike = create_user "Mike Peterson", "mike@37signals.com"
|
||||
|
||||
# Array of authors for random assignment
|
||||
authors = [ david, jason, kevin, sarah, mike ]
|
||||
authors = [ david, jason, jz, kevin, jorge, mike ]
|
||||
|
||||
# Card titles for reuse across collections
|
||||
card_titles = [
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<title>The server cannot process the request due to a client error (400 Bad Request)</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<style>
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #FFF;
|
||||
color: #261B23;
|
||||
display: grid;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: clamp(1rem, 2.5vw, 2rem);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.0925em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: 2em;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main header {
|
||||
width: min(100%, 12em);
|
||||
}
|
||||
|
||||
main header svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main article {
|
||||
width: min(100%, 30em);
|
||||
}
|
||||
|
||||
main article p {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- This file lives in public/400.html -->
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm140.456 133.2831c-40.823 0-64.884-35.146-64.884-85.7015 0-50.5554 24.061-85.700907 64.884-85.700907 40.822 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.062 85.7015-64.884 85.7015zm0-133.2831c-17.573 0-22.71 21.8984-22.71 47.5816 0 25.6835 5.137 47.5815 22.71 47.5815 17.302 0 22.709-21.898 22.709-47.5815 0-25.6832-5.407-47.5816-22.709-47.5816z" fill="#f0eff0"/><path d="m123.606 85.4445c3.212 1.0523 5.538 4.2089 5.538 8.0301 0 6.1472-4.209 9.5254-11.298 9.5254h-15.617v-34.0033h14.565c7.089 0 11.353 3.1566 11.353 9.2484 0 3.6551-2.049 6.3134-4.541 7.1994zm-12.904-2.9905h5.095c2.603 0 3.988-.9968 3.988-3.1013 0-2.1044-1.385-3.0459-3.988-3.0459h-5.095zm0 6.6456v6.5902h5.981c2.492 0 3.877-1.3291 3.877-3.2674 0-2.049-1.385-3.3228-3.877-3.3228zm43.786 13.9004h-8.362v-1.274c-.831.831-3.323 1.717-5.981 1.717-4.929 0-9.083-2.769-9.083-8.0301 0-4.818 4.154-7.9193 9.581-7.9193 2.049 0 4.486.6646 5.483 1.3845v-1.606c0-1.606-.942-2.9905-3.046-2.9905-1.606 0-2.548.7199-2.935 1.8275h-8.197c.72-4.8181 4.985-8.6393 11.409-8.6393 7.088 0 11.131 3.7659 11.131 10.2453zm-8.362-6.9779v-1.4399c-.554-1.0522-2.049-1.7167-3.655-1.7167-1.717 0-3.434.7199-3.434 2.3813 0 1.7168 1.717 2.4367 3.434 2.4367 1.606 0 3.101-.6645 3.655-1.6614zm27.996 6.9779v-1.994c-1.163 1.329-3.599 2.548-6.147 2.548-7.199 0-11.131-5.8151-11.131-13.0145s3.932-13.0143 11.131-13.0143c2.548 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.664-1.3291-2.159-2.326-3.821-2.326-2.99 0-4.763 2.4368-4.763 5.6488s1.773 5.5934 4.763 5.5934c1.717 0 3.157-.9415 3.821-2.326zm35.471-2.049h-3.101v11.2421h-8.806v-34.0033h15.285c7.31 0 12.35 4.1535 12.35 11.5744 0 5.1503-2.603 8.6947-6.757 10.2453l7.975 12.1836h-9.858zm-3.101-15.2849v8.1962h5.538c3.156 0 4.596-1.606 4.596-4.0981s-1.44-4.0981-4.596-4.0981zm36.957 17.8323h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.962 5.095 11.962 12.5159v2.1598h-16.115c.277 2.9905 1.827 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm30.98 27.5234v-10.799c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.9259-11.132-13.0145 0-7.144 3.932-13.0143 11.132-13.0143 2.547 0 4.984 1.2184 6.147 2.5475v-1.9937h8.695v33.726zm0-17.9981v-6.5902c-.665-1.3291-2.105-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.661 0 3.156-.9415 3.821-2.326zm36.789-15.7279v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.996 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm19.084 16.2263h8.03c-.886 5.7597-5.206 9.2487-11.685 9.2487-7.643 0-12.682-5.2613-12.682-13.0145 0-7.6978 5.316-13.0143 12.515-13.0143 7.643 0 11.963 5.095 11.963 12.5159v2.1598h-16.116c.277 2.9905 1.828 4.5965 4.32 4.5965 1.772 0 3.156-.7753 3.655-2.4921zm-3.822-10.0237c-2.049 0-3.433 1.2737-3.987 3.5997h7.532c-.111-2.0491-1.385-3.5997-3.545-3.5997zm13.428 11.0206h8.474c.387 1.3845 1.606 2.1598 3.156 2.1598 1.44 0 2.548-.5538 2.548-1.7168 0-.9414-.72-1.2737-1.939-1.5506l-4.873-.9969c-4.154-.886-6.867-2.8797-6.867-7.2547 0-5.3165 4.762-8.4178 10.633-8.4178 6.812 0 10.522 3.1567 11.297 8.0855h-8.03c-.277-1.0522-1.052-1.9937-3.046-1.9937-1.273 0-2.326.5538-2.326 1.6614 0 .7753.554 1.163 1.717 1.3845l4.929 1.163c4.541 1.0522 6.978 3.4335 6.978 7.4763 0 5.3168-4.818 8.2518-10.91 8.2518-6.369 0-10.965-2.88-11.741-8.2518zm27.538-.8861v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.993-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.871 0-9.193-2.769-9.193-9.0819z" fill="#d30001"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>The server cannot process the request due to a client error.</strong> Please check the request and try again. If you’re the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -112,14 +112,6 @@
|
||||
<article>
|
||||
<p><strong>The page you were looking for doesn’t exist.</strong> You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
<article>
|
||||
<p>You may want to try one of these subdomains:</p>
|
||||
<ul>
|
||||
<li><a href="https://37s.fizzy.37signals.com/">37s.fizzy.37signals.com</a></li>
|
||||
<li><a href="https://dev.fizzy.37signals.com/">dev.fizzy.37signals.com</a></li>
|
||||
<li><a href="https://qa.fizzy.37signals.com/">qa.fizzy.37signals.com</a></li>
|
||||
</ul>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
+108
-61
File diff suppressed because one or more lines are too long
+108
-60
File diff suppressed because one or more lines are too long
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
ActiveRecord::Base.logger = Logger.new(File::NULL)
|
||||
|
||||
class BootstrapSignalId
|
||||
def initialize(dry_run: false)
|
||||
@dry_run = dry_run
|
||||
end
|
||||
|
||||
def run
|
||||
SignalId::Database.on_master do
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "\n# tenant: #{tenant}"
|
||||
|
||||
next unless check_account_preconditions
|
||||
create_signal_id_account if Account.first.queenbee_id.nil?
|
||||
|
||||
create_signal_id_users
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_account_preconditions
|
||||
unless Account.count == 1
|
||||
puts "There are #{Account.count} accounts, but exactly one is expected."
|
||||
false
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def create_signal_id_account
|
||||
owner = SignalId::Identity.find_by_email_address!("kevin@37signals.com")
|
||||
print_identity("New owner is:", owner)
|
||||
|
||||
unless @dry_run
|
||||
qbattr = queenbee_account_attributes(owner)
|
||||
queenbee_account = Queenbee::Remote::Account.create!(qbattr)
|
||||
|
||||
signal_id_account = SignalId::Account.find_by!(queenbee_id: queenbee_account.id)
|
||||
signal_id_account.update_column :subdomain, ApplicationRecord.current_tenant
|
||||
|
||||
account = Account.first
|
||||
account.queenbee_id = queenbee_account.id
|
||||
account.name = ApplicationRecord.current_tenant
|
||||
account.save!
|
||||
end
|
||||
end
|
||||
|
||||
def create_signal_id_users
|
||||
signal_account = Account.first.signal_account
|
||||
|
||||
User.find_each do |user|
|
||||
if !user.system? && user.signal_user_id.nil?
|
||||
signal_identities = SignalId::Identity.where(email_address: user.email_address)
|
||||
if signal_identities.length > 1
|
||||
puts "Multiple identities found for #{user.email_address}:"
|
||||
signal_identities.each { |identity| print_identity(" - ", identity) }
|
||||
signal_identity = signal_identities.first
|
||||
elsif signal_identities.length == 1
|
||||
signal_identity = signal_identities.first
|
||||
print_identity("Identity for #{user.email_address}:", signal_identity)
|
||||
else
|
||||
puts "No identity found for #{user.name} (#{user.email_address})"
|
||||
signal_identity = nil
|
||||
end
|
||||
|
||||
if signal_identity
|
||||
unless @dry_run
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
|
||||
user.signal_user_id = signal_user.id
|
||||
user.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def print_identity(message = "Identity:", identity)
|
||||
pad = " " * message.length
|
||||
puts "#{message} #{identity.name} (#{identity.email_address})"
|
||||
puts "#{pad } ID: #{identity.id}"
|
||||
puts "#{pad } Username: #{identity.username}"
|
||||
end
|
||||
|
||||
def queenbee_account_attributes(signal_identity)
|
||||
{
|
||||
skip_remote: true, # Fizzy creates its own local account
|
||||
product_name: "fizzy",
|
||||
name: account_name,
|
||||
owner_identity_id: signal_identity.id,
|
||||
trial: false,
|
||||
subscription: subscription_attributes,
|
||||
remote_request: request_attributes
|
||||
}
|
||||
end
|
||||
|
||||
def subscription_attributes
|
||||
subscription = FreeV1Subscription
|
||||
{ name: subscription.to_param, price: subscription.price }
|
||||
end
|
||||
|
||||
def request_attributes
|
||||
{ user_agent: "script/bootstrap-signal-id.rb" }
|
||||
end
|
||||
|
||||
def account_name
|
||||
name = ApplicationRecord.current_tenant
|
||||
name += " (Beta)" if Rails.env.beta?
|
||||
name
|
||||
end
|
||||
end
|
||||
|
||||
BootstrapSignalId.new(dry_run: false).run
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
CHANGES = [
|
||||
{ from: "david@37signals.com", to: "david@hey.com" },
|
||||
{ from: "jay@37signals.com", to: "jay@basecamp.com" },
|
||||
{ from: "jeremy@37signals.com", to: "jeremy@basecamp.com" },
|
||||
{ from: "jillian@37signals.com", to: "jillian@basecamp.com" },
|
||||
{ from: "jorge@37signals.com", to: "jorge@basecamp.com" },
|
||||
{ from: "merissa@37signals.com", to: "merissa@basecamp.com" },
|
||||
{ from: "michelle@37signals.com", to: "michelle@basecamp.com" },
|
||||
{ from: "scott@37signals.com", to: "scott@basecamp.com" },
|
||||
{ from: "silvia@37signals.com", to: "silvia@basecamp.com" }
|
||||
]
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
CHANGES.each do |change|
|
||||
user = User.find_by(email_address: change[:from])
|
||||
if user
|
||||
puts "Updating user #{user.id} in tenant #{tenant}: #{change[:from]} -> #{change[:to]}"
|
||||
user.email_address = change[:to]
|
||||
user.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,29 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class FirstRunsControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
Account.destroy_all
|
||||
end
|
||||
|
||||
test "show" do
|
||||
get first_run_path
|
||||
assert_response :ok
|
||||
assert_select "title", text: "Set up Fizzy"
|
||||
end
|
||||
|
||||
test "show after completion" do
|
||||
Account.create! name: "Fizzy"
|
||||
get first_run_path
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
test "create" do
|
||||
assert_difference -> { Account.count }, +1 do
|
||||
post first_run_path, params: { user: { name: "New", email_address: "new@37signals.com", password: "secret123456" } }
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
follow_redirect!
|
||||
assert_response :ok
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
require "test_helper"
|
||||
|
||||
class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
|
||||
test "show renders when not signed in" do
|
||||
get session_launchpad_path(params: { sig: "test-sig" })
|
||||
|
||||
assert_response :success
|
||||
|
||||
assert_equal cookies[:_fizzy_launchpad_sig], "test-sig"
|
||||
end
|
||||
|
||||
test "create establishes a session when the sig is valid" do
|
||||
user = users(:david)
|
||||
cookies[:_fizzy_launchpad_sig] = user.signal_user.perishable_signature
|
||||
|
||||
put session_launchpad_path
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert parsed_cookies.signed[:session_token]
|
||||
end
|
||||
|
||||
test "returns 401 when the sig is invalid" do
|
||||
user = users(:david)
|
||||
cookies[:sig] = "not-valid"
|
||||
|
||||
put session_launchpad_path
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,108 @@
|
||||
require "test_helper"
|
||||
|
||||
class Signup::AccountsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "new under a tenanted domain redirects to the root" do
|
||||
get new_signup_account_url
|
||||
|
||||
assert_redirected_to root_url
|
||||
end
|
||||
|
||||
test "new under an untenanted domain is OK" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
get new_signup_account_url, headers: auth_headers
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create with invalid params" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
post signup_accounts_url,
|
||||
headers: auth_headers,
|
||||
params: { signup: { full_name: "Jim", email_address: "jim@example.com", password: "", company_name: "" } }
|
||||
|
||||
assert_response :unprocessable_entity
|
||||
assert_select "div.alert--error", text: /you need to choose a password/i
|
||||
end
|
||||
|
||||
test "create for a new " do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
assert_difference -> { SignalId::Identity.count }, +1 do
|
||||
assert_difference -> { SignalId::Account.count }, +1 do
|
||||
post signup_accounts_url, headers: auth_headers,
|
||||
params: {
|
||||
signup: {
|
||||
full_name: "Jim",
|
||||
email_address: "jim@example.com",
|
||||
password: SecureRandom.hex(12),
|
||||
company_name: "signup-accounts-controller-test-1"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
signal_account = SignalId::Account.last
|
||||
assert_redirected_to(/#{signal_account.login_url}/)
|
||||
end
|
||||
|
||||
test "create for an existing identity" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
identity = signal_identities(:david)
|
||||
|
||||
post signup_accounts_url, headers: auth_headers,
|
||||
params: { signup: { email_address: identity.email_address, company_name: "signup-accounts-controller-test-2" } }
|
||||
|
||||
assert_authentication_requested_for identity
|
||||
|
||||
assert_no_difference -> { SignalId::Identity.count } do
|
||||
assert_difference -> { SignalId::Account.count } do
|
||||
authenticate_via_launchpad_as(identity)
|
||||
assert_redirected_to_account
|
||||
assert_nil session[:signup]
|
||||
end
|
||||
end
|
||||
|
||||
signal_account = SignalId::Account.last
|
||||
ApplicationRecord.with_tenant(signal_account.subdomain) do
|
||||
assert_equal Account.last, signal_account.peer
|
||||
end
|
||||
end
|
||||
|
||||
test "actions require HTTP basic authentication while we're in internal-only mode" do
|
||||
integration_session.host = "example.com" # no subdomain
|
||||
|
||||
get new_signup_account_url
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
private
|
||||
def auth_headers
|
||||
{
|
||||
"HTTP_AUTHORIZATION" => ActionController::HttpAuthentication::Basic.encode_credentials(
|
||||
Rails.application.credentials.dig(:account_signup_http_basic_auth, :name),
|
||||
Rails.application.credentials.dig(:account_signup_http_basic_auth, :password)
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
def assert_authentication_requested_for(identity)
|
||||
assert_response :redirect
|
||||
assert_match(/#{Regexp.escape(Launchpad.url("/authenticate", login_hint: identity.email_address))}.*&purpose=signup/, redirect_to_url)
|
||||
end
|
||||
|
||||
def authenticate_via_launchpad_as(identity)
|
||||
get signup_session_url, headers: auth_headers, params: { sig: identity.perishable_signature }
|
||||
assert_equal identity.id, session[:signup]["identity_id"]
|
||||
assert_redirected_to new_signup_completion_url
|
||||
post signup_completions_url, headers: auth_headers
|
||||
end
|
||||
|
||||
def assert_redirected_to_account(signal_account = SignalId::Account.last)
|
||||
assert_response :redirect
|
||||
assert_match(/#{Regexp.escape(signal_account.url("/session/launchpad"))}.*&sig=/, redirect_to_url)
|
||||
end
|
||||
end
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
37s:
|
||||
name: 37signals
|
||||
join_code: "ejpP-THlQ-Cc2f"
|
||||
queenbee_id: <%= ActiveRecord::FixtureSet.identify :'37s_fizzy' %>
|
||||
|
||||
Vendored
+3
@@ -5,18 +5,21 @@ david:
|
||||
email_address: david@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
signal_user: 37s_fizzy_david
|
||||
|
||||
jz:
|
||||
name: JZ
|
||||
email_address: jz@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
signal_user: 37s_fizzy_jzimdars
|
||||
|
||||
kevin:
|
||||
name: Kevin
|
||||
email_address: kevin@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: admin
|
||||
signal_user: 37s_fizzy_kevin
|
||||
|
||||
system:
|
||||
name: System
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
require "test_helper"
|
||||
|
||||
class Account::SignalAccountTest < ActiveSupport::TestCase
|
||||
# # TODO(MIKE): Queenbee client API compliance tests
|
||||
# include Queenbee::Testing::Client
|
||||
|
||||
setup do
|
||||
@account = accounts("37s")
|
||||
end
|
||||
|
||||
test "belongs to a signal_account via a shared queenbee_id" do
|
||||
assert_not_nil @account.queenbee_id
|
||||
assert_equal @account.queenbee_id, Account.new(signal_account: @account.signal_account).queenbee_id
|
||||
assert_equal @account.signal_account, Account.new(queenbee_id: @account.queenbee_id).signal_account
|
||||
end
|
||||
|
||||
test ".create_with_admin_user creates a new local account and user peers" do
|
||||
ApplicationRecord.create_tenant("account-create-with-dependents") do
|
||||
signal_account = signal_accounts(:honcho_fizzy)
|
||||
account = Account.create_with_admin_user(queenbee_id: signal_account.queenbee_id)
|
||||
|
||||
assert_not_nil account
|
||||
assert account.persisted?
|
||||
assert_equal 1, Account.count
|
||||
assert_equal signal_account.queenbee_id, account.queenbee_id
|
||||
assert_equal signal_account.name, account.name
|
||||
assert_equal account, signal_account.peer
|
||||
|
||||
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.signal_user_id
|
||||
assert_equal "admin", user.role
|
||||
assert_equal user, signal_account.owner.peer
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user