Merge branch 'main' into theme-switcher-revised

This commit is contained in:
Jason Zimdars
2025-12-10 16:30:36 -06:00
committed by GitHub
137 changed files with 2744 additions and 340 deletions
+8
View File
@@ -6,6 +6,14 @@
# Ignore bundler config.
/.bundle
# Ignore documentation
/docs/
/README.md
/CLAUDE.md
/AGENTS.md
/STYLE.md
/CONTRIBUTING.md
# Ignore all environment files (except templates).
/.env*
!/.env*.erb
-1
View File
@@ -30,4 +30,3 @@ updates:
open-pull-requests-limit: 10
cooldown:
default-days: 7
semver-major-days: 14
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
@@ -33,7 +33,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version
+6 -6
View File
@@ -39,13 +39,13 @@ jobs:
IMAGE_NAME: ${{ github.repository }}
steps:
- name: Checkout
uses: actions/checkout@v5.0.0
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -62,7 +62,7 @@ jobs:
- name: Extract Docker metadata (tags, labels) with arch suffix
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@v5.10.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
@@ -118,7 +118,7 @@ jobs:
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to GHCR
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -135,7 +135,7 @@ jobs:
- name: Compute base tags (no suffix)
id: meta
uses: docker/metadata-action@v5.8.0
uses: docker/metadata-action@v5.10.0
with:
images: ${{ steps.vars.outputs.canonical }}
tags: |
@@ -179,7 +179,7 @@ jobs:
done <<< "$tags"
- name: Install Cosign
uses: sigstore/cosign-installer@v3.9.2
uses: sigstore/cosign-installer@v4.0.0
- name: Cosign sign all tags (keyless OIDC)
shell: bash
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
- name: Install system packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y libsqlite3-0 libvips curl ffmpeg
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ruby/setup-ruby@v1
with:
+2
View File
@@ -6,4 +6,6 @@ paths = [
'''log''',
'''tmp''',
'''.*\.yml\.enc''',
'''docs/''',
'''test/''',
]
-6
View File
@@ -1,9 +1,3 @@
d8463077:gems/fizzy-saas/bin/setup:generic-api-key:54
c4073c1c:app/models/integration/basecamp.rb:generic-api-key:3
c4073c1c:app/models/integration/basecamp.rb:generic-api-key:4
02a42167:test/models/webhook_test.rb:slack-webhook-url:57
2fc9215b:test/models/webhook/delivery_test.rb:slack-webhook-url:156
2fc9215b:test/models/webhook_test.rb:slack-webhook-url:57
a515ea3b:test/fixtures/webhooks.yml:generic-api-key:5
a515ea3b:test/fixtures/webhooks.yml:generic-api-key:11
1f21c12c:test/vcr_cassettes/command/ai/translator_test-test_combine_commands_and_filters.yml:github-oauth:73012
+3 -2
View File
@@ -1,8 +1,9 @@
source "https://rubygems.org"
gem "rails", github: "rails/rails", branch: "main"
git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "rails", github: "rails/rails", branch: "ast-immediate-variants-process-locally"
# Assets & front end
gem "importmap-rails"
gem "propshaft"
@@ -27,7 +28,7 @@ gem "rqrcode"
gem "redcarpet"
gem "rouge"
gem "jbuilder"
gem "lexxy"
gem "lexxy", bc: "lexxy"
gem "image_processing", "~> 1.14"
gem "platform_agent"
gem "aws-sdk-s3", require: false
+10 -5
View File
@@ -1,3 +1,10 @@
GIT
remote: https://github.com/basecamp/lexxy
revision: d292280b6d2c5d8a924327adf8bb9f0b25953539
specs:
lexxy (0.1.23.beta)
rails (>= 8.0.2)
GIT
remote: https://github.com/basecamp/useragent
revision: 433ca320a42db1266c4b89df74d0abdb9a880c5e
@@ -6,8 +13,8 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: 3d105fc346fbb3121bbcf6340f2b19104bf326f0
branch: main
revision: 0636a79d1bf268db6cdbbc6327106d08c3ff3751
branch: ast-immediate-variants-process-locally
specs:
actioncable (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
@@ -237,8 +244,6 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.23.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
@@ -518,7 +523,7 @@ DEPENDENCIES
jbuilder
kamal
letter_opener
lexxy
lexxy!
mission_control-jobs
mittens
mocha
+10 -5
View File
@@ -41,6 +41,13 @@ GIT
yabeda-puma-plugin
yabeda-rails (>= 0.10)
GIT
remote: https://github.com/basecamp/lexxy
revision: d292280b6d2c5d8a924327adf8bb9f0b25953539
specs:
lexxy (0.1.23.beta)
rails (>= 8.0.2)
GIT
remote: https://github.com/basecamp/queenbee-plugin
revision: 15faf03a876c5e66b67753d2e1ddb24f1eb5abb2
@@ -75,8 +82,8 @@ GIT
GIT
remote: https://github.com/rails/rails.git
revision: 3d105fc346fbb3121bbcf6340f2b19104bf326f0
branch: main
revision: 0636a79d1bf268db6cdbbc6327106d08c3ff3751
branch: ast-immediate-variants-process-locally
specs:
actioncable (8.2.0.alpha)
actionpack (= 8.2.0.alpha)
@@ -313,8 +320,6 @@ GEM
logger (~> 1.6)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
lexxy (0.1.23.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
loofah (2.24.1)
@@ -653,7 +658,7 @@ DEPENDENCIES
jbuilder
kamal
letter_opener
lexxy
lexxy!
mission_control-jobs
mittens
mocha
+22 -5
View File
@@ -14,7 +14,7 @@ This repo contains a starter deployment file that you can modify for your own sp
The steps to configure your very own Fizzy are:
1. Fork the repo
2. Edit few things in config/deploy.yml, .kamal/secrets, and config/environments/production.rb
2. Edit few things in config/deploy.yml and .kamal/secrets
3. Run `kamal setup` to do your first deploy.
We'll go through each of these in turn.
@@ -37,6 +37,7 @@ We've added comments to that file to highlight what each setting needs to be, bu
- `ssh/user`: If you access your server a `root` you can leave this alone; if you use a different user, set it here.
- `proxy/ssl` and `proxy/host`: Kamal can set up SSL certificates for you automatically. To enable that, set the hostname again as `host`. If you don't want SSL for some reason, you can set `ssl: false` to turn it off.
- `env/clear/MAILER_FROM_ADDRESS`: This is the email address that Fizzy will send emails from. It should usually be an address from the same domain where you're running Fizzy.
- `env/clear/SMTP_ADDRESS`: The address of an SMTP server that you can send email through. You can use a 3rd-party service for this, like Sendgrid or Postmark, in which case their documentation will tell you what to use for this.
Fizzy also requires a few environment variables to be set up, some of which contain secrets.
The simplest way to do this is to put them in a file called `.kamal/secrets`.
@@ -58,6 +59,7 @@ SMTP_PASSWORD=email-provider-password
The values you enter here will be specific to you, and you can get or create them as follows:
- `SECRET_KEY_BASE` should be a long, random secret. You can run `bin/rails secret` to create a suitable value for this.
- `SMTP_USERNAME` & `SMTP_PASSWORD` should be valid credentials for your SMTP server. If you're using a 3rd-party service here, consult their documentation for what to use.
- `VAPID_PUBLIC_KEY` & `VAPID_PRIVATE_KEY` are a pair of credentials that are used for sending notifications. You can create your own keys by starting a development console with:
```sh
@@ -73,10 +75,6 @@ The values you enter here will be specific to you, and you can get or create the
puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
```
- `SMTP_USERNAME`/`SMTP_PASSWORD` are credentials you should get from your email provider.
Lastly, you'll need to set up the rest of your email configuration in `config/environments/production.rb`. There is an example configuration in comments at the top of that file. The actual settings you use here will depend on your email provider, but in most cases will look similar to that section, so you can uncomment it and edit to suit. Note that it will use the `SMTP_USERNAME` and `SMTP_PASSWORD` values you entered in your secrets.
Once you've made all those changes, commit them to your fork so they're saved.
### Deploy Fizzy!
@@ -95,6 +93,25 @@ After the first deploy is done, any subsequent steps won't need to do that initi
bin/kamal deploy
```
## File storage (Active Storage)
Production uses the local disk service by default. To use any other service defined in `config/storage.yml`, set `ACTIVE_STORAGE_SERVICE`.
To use the included `s3` service, set:
- `ACTIVE_STORAGE_SERVICE=s3`
- `S3_ACCESS_KEY_ID`
- `S3_BUCKET` (defaults to `fizzy-#{Rails.env}-activestorage`)
- `S3_REGION` (defaults to `us-east-1`)
- `S3_SECRET_ACCESS_KEY`
Optional for S3-compatible endpoints:
- `S3_ENDPOINT`
- `S3_FORCE_PATH_STYLE=true`
- `S3_REQUEST_CHECKSUM_CALCULATION` (defaults to `when_supported`)
- `S3_RESPONSE_CHECKSUM_VALIDATION` (defaults to `when_supported`)
## Development
### Setting up
+19
View File
@@ -0,0 +1,19 @@
.access_tokens_table {
border-collapse: collapse;
inline-size: 100%;
td, th {
border-block-end: 1px solid var(--color-ink-light);
padding-inline: var(--inline-space);
text-align: start;
}
th {
font-size: var(--text-x-small);
text-transform: uppercase;
}
tr:nth-of-type(even) {
background-color: var(--color-ink-lightest);
}
}
+4
View File
@@ -304,6 +304,10 @@
max-height: 200px;
overflow: scroll;
&:is(.lexxy-prompt-menu--visible) {
padding: var(--lexxy-prompt-padding);
}
}
.lexxy-prompt-menu--visible {
+9
View File
@@ -225,6 +225,15 @@
}
}
.nav__blank-slate {
font-size: var(--text-small);
margin: 1rem auto;
.nav:has(.popup__item:not([hidden])) & {
display: none;
}
}
.nav__footer {
background-color: var(--color-canvas);
border-block-start: 1px solid var(--color-ink-lighter);
+2
View File
@@ -116,6 +116,8 @@
pointer-events: none;
}
.cursor-pointer { cursor: pointer; }
/* Padding */
.pad { padding: var(--block-space) var(--inline-space); }
.pad-double { padding: var(--block-space-double) var(--inline-space-double); }
@@ -9,8 +9,11 @@ class Account::JoinCodesController < ApplicationController
end
def update
@join_code.update!(join_code_params)
redirect_to account_join_code_path
if @join_code.update(join_code_params)
redirect_to account_join_code_path
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@@ -3,6 +3,11 @@ class Boards::ColumnsController < ApplicationController
before_action :set_column, only: %i[ show update destroy ]
def index
@columns = @board.columns.sorted
fresh_when etag: @columns
end
def show
set_page_and_extract_portion_from @column.cards.active.latest.with_golden_first.preloaded
fresh_when etag: @page.records
@@ -10,14 +15,29 @@ class Boards::ColumnsController < ApplicationController
def create
@column = @board.columns.create!(column_params)
respond_to do |format|
format.turbo_stream
format.json { head :created, location: board_column_path(@board, @column, format: :json) }
end
end
def update
@column.update!(column_params)
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@column.destroy
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
private
+24 -7
View File
@@ -1,9 +1,13 @@
class BoardsController < ApplicationController
include FilterScoped
before_action :set_board, except: %i[ new create ]
before_action :set_board, except: %i[ index new create ]
before_action :ensure_permission_to_admin_board, only: %i[ update destroy ]
def index
set_page_and_extract_portion_from Current.user.boards
end
def show
if @filter.used?(ignore_boards: true)
show_filtered_cards
@@ -18,7 +22,11 @@ class BoardsController < ApplicationController
def create
@board = Board.create! board_params.with_defaults(all_access: true)
redirect_to board_path(@board)
respond_to do |format|
format.html { redirect_to board_path(@board) }
format.json { head :created, location: board_path(@board, format: :json) }
end
end
def edit
@@ -31,16 +39,25 @@ class BoardsController < ApplicationController
@board.update! board_params
@board.accesses.revise granted: grantees, revoked: revokees if grantees_changed?
if @board.accessible_to?(Current.user)
redirect_to edit_board_path(@board), notice: "Saved"
else
redirect_to root_path, notice: "Saved (you were removed from the board)"
respond_to do |format|
format.html do
if @board.accessible_to?(Current.user)
redirect_to edit_board_path(@board), notice: "Saved"
else
redirect_to root_path, notice: "Saved (you were removed from the board)"
end
end
format.json { head :no_content }
end
end
def destroy
@board.destroy
redirect_to root_path
respond_to do |format|
format.html { redirect_to root_path }
format.json { head :no_content }
end
end
private
@@ -9,5 +9,10 @@ class Cards::AssignmentsController < ApplicationController
def create
@card.toggle_assignment @board.users.active.find(params[:assignee_id])
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
+5 -1
View File
@@ -11,7 +11,11 @@ class Cards::BoardsController < ApplicationController
def update
@card.move_to(@board)
redirect_to @card
respond_to do |format|
format.html { redirect_to @card }
format.json { head :no_content }
end
end
private
+10 -2
View File
@@ -3,11 +3,19 @@ class Cards::ClosuresController < ApplicationController
def create
@card.close
render_card_replacement
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
def destroy
@card.reopen
render_card_replacement
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
end
@@ -13,10 +13,20 @@ class Cards::Comments::ReactionsController < ApplicationController
def create
@reaction = @comment.reactions.create!(params.expect(reaction: :content))
respond_to do |format|
format.turbo_stream
format.json { head :created }
end
end
def destroy
@reaction.destroy
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
private
@@ -4,8 +4,17 @@ class Cards::CommentsController < ApplicationController
before_action :set_comment, only: %i[ show edit update destroy ]
before_action :ensure_creatorship, only: %i[ edit update destroy ]
def index
set_page_and_extract_portion_from @card.comments.chronologically
end
def create
@comment = @card.comments.create!(comment_params)
respond_to do |format|
format.turbo_stream
format.json { head :created, location: card_comment_path(@card, @comment, format: :json) }
end
end
def show
@@ -16,10 +25,20 @@ class Cards::CommentsController < ApplicationController
def update
@comment.update! comment_params
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@comment.destroy
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
private
+10 -2
View File
@@ -3,11 +3,19 @@ class Cards::GoldnessesController < ApplicationController
def create
@card.gild
render_card_replacement
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
def destroy
@card.ungild
render_card_replacement
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
end
+5 -1
View File
@@ -3,6 +3,10 @@ class Cards::ImagesController < ApplicationController
def destroy
@card.image.purge_later
redirect_to @card
respond_to do |format|
format.html { redirect_to @card }
format.json { head :no_content }
end
end
end
+5 -1
View File
@@ -3,6 +3,10 @@ class Cards::NotNowsController < ApplicationController
def create
@card.postpone
render_card_replacement
respond_to do |format|
format.turbo_stream { render_card_replacement }
format.json { head :no_content }
end
end
end
+15
View File
@@ -5,6 +5,11 @@ class Cards::StepsController < ApplicationController
def create
@step = @card.steps.create!(step_params)
respond_to do |format|
format.turbo_stream
format.json { head :created, location: card_step_path(@card, @step, format: :json) }
end
end
def show
@@ -15,10 +20,20 @@ class Cards::StepsController < ApplicationController
def update
@step.update!(step_params)
respond_to do |format|
format.turbo_stream
format.json { render :show }
end
end
def destroy
@step.destroy!
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
private
@@ -9,6 +9,11 @@ class Cards::TaggingsController < ApplicationController
def create
@card.toggle_tag_with sanitized_tag_title_param
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
private
+9 -2
View File
@@ -5,11 +5,18 @@ class Cards::TriagesController < ApplicationController
column = @card.board.columns.find(params[:column_id])
@card.triage_into(column)
redirect_to @card
respond_to do |format|
format.html { redirect_to @card }
format.json { head :no_content }
end
end
def destroy
@card.send_back_to_triage
redirect_to @card
respond_to do |format|
format.html { redirect_to @card }
format.json { head :no_content }
end
end
end
@@ -7,9 +7,19 @@ class Cards::WatchesController < ApplicationController
def create
@card.watch_by Current.user
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@card.unwatch_by Current.user
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
+22 -3
View File
@@ -10,8 +10,18 @@ class CardsController < ApplicationController
end
def create
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
redirect_to card
respond_to do |format|
format.html do
card = @board.cards.find_or_create_by!(creator: Current.user, status: "drafted")
redirect_to card
end
format.json do
card = @board.cards.create! card_params.merge(creator: Current.user)
card.publish
head :created, location: card_path(card, format: :json)
end
end
end
def show
@@ -22,11 +32,20 @@ class CardsController < ApplicationController
def update
@card.update! card_params
respond_to do |format|
format.turbo_stream
format.json { render :show }
end
end
def destroy
@card.destroy!
redirect_to @card.board, notice: "Card deleted"
respond_to do |format|
format.html { redirect_to @card.board, notice: "Card deleted" }
format.json { head :no_content }
end
end
private
+13 -3
View File
@@ -7,7 +7,7 @@ module Authentication
after_action :ensure_development_magic_link_not_leaked
helper_method :authenticated?
etag { Current.session.id if authenticated? }
etag { Current.identity.id if authenticated? }
include LoginHelper
end
@@ -32,7 +32,7 @@ module Authentication
private
def authenticated?
Current.session.present?
Current.identity.present?
end
def require_account
@@ -42,7 +42,7 @@ module Authentication
end
def require_authentication
resume_session || request_authentication
resume_session || authenticate_by_bearer_token || request_authentication
end
def resume_session
@@ -55,6 +55,16 @@ module Authentication
Session.find_signed(cookies.signed[:session_token])
end
def authenticate_by_bearer_token
if request.authorization.to_s.include?("Bearer")
authenticate_or_request_with_http_token do |token|
if identity = Identity.find_by_permissable_access_token(token, method: request.method)
Current.identity = identity
end
end
end
end
def request_authentication
if Current.account.present?
session[:return_to_after_authenticating] = request.url
@@ -12,7 +12,7 @@ module RequestForgeryProtection
end
def verified_request?
super || safe_fetch_site?
super || safe_fetch_site? || request.format.json?
end
SAFE_FETCH_SITES = %w[ same-origin same-site ]
@@ -0,0 +1,40 @@
class My::AccessTokensController < ApplicationController
def index
@access_tokens = my_access_tokens.order(created_at: :desc)
end
def show
@access_token = my_access_tokens.find(verifier.verify(params[:id]))
rescue ActiveSupport::MessageVerifier::InvalidSignature
redirect_to my_access_tokens_path, alert: "Token is no longer visible"
end
def new
@access_token = my_access_tokens.new
end
def create
access_token = my_access_tokens.create!(access_token_params)
expiring_id = verifier.generate access_token.id, expires_in: 10.seconds
redirect_to my_access_token_path(expiring_id)
end
def destroy
my_access_tokens.find(params[:id]).destroy!
redirect_to my_access_tokens_path
end
private
def my_access_tokens
Current.identity.access_tokens
end
def access_token_params
params.expect(access_token: %i[ description permission ])
end
def verifier
Rails.application.message_verifier(:access_tokens)
end
end
@@ -0,0 +1,7 @@
class My::IdentitiesController < ApplicationController
disallow_account_scope
def show
@identity = Current.identity
end
end
@@ -2,10 +2,15 @@ class Notifications::BulkReadingsController < ApplicationController
def create
Current.user.notifications.unread.read_all
if from_tray?
head :ok
else
redirect_to notifications_path
respond_to do |format|
format.html do
if from_tray?
head :ok
else
redirect_to notifications_path
end
end
format.json { head :no_content }
end
end
@@ -2,10 +2,20 @@ class Notifications::ReadingsController < ApplicationController
def create
@notification = Current.user.notifications.find(params[:notification_id])
@notification.read
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
def destroy
@notification = Current.user.notifications.find(params[:notification_id])
@notification.unread
respond_to do |format|
format.turbo_stream
format.json { head :no_content }
end
end
end
+8 -1
View File
@@ -1,13 +1,20 @@
class NotificationsController < ApplicationController
MAX_UNREAD_NOTIFICATIONS = 500
MAX_UNREAD_NOTIFICATIONS_VIA_API = 100
def index
@unread = Current.user.notifications.unread.ordered.preloaded.limit(MAX_UNREAD_NOTIFICATIONS) unless current_page_param
@unread = Current.user.notifications.unread.ordered.preloaded.limit(max_unread_notifications) unless current_page_param
set_page_and_extract_portion_from Current.user.notifications.read.ordered.preloaded
respond_to do |format|
format.turbo_stream if current_page_param # Allows read-all action to side step pagination
format.html
format.json
end
end
private
def max_unread_notifications
request.format.json? ? MAX_UNREAD_NOTIFICATIONS_VIA_API : MAX_UNREAD_NOTIFICATIONS
end
end
+5
View File
@@ -0,0 +1,5 @@
class TagsController < ApplicationController
def index
set_page_and_extract_portion_from Current.account.tags.alphabetically
end
end
@@ -5,13 +5,7 @@ class Users::PushSubscriptionsController < ApplicationController
end
def create
if subscription = @push_subscriptions.find_by(push_subscription_params)
subscription.touch
else
@push_subscriptions.create! push_subscription_params.merge(user_agent: request.user_agent)
end
head :ok
@push_subscriptions.create_with(user_agent: request.user_agent).create_or_find_by!(push_subscription_params)
end
def destroy
+18 -4
View File
@@ -1,7 +1,11 @@
class UsersController < ApplicationController
before_action :set_user
before_action :set_user, except: %i[ index ]
before_action :ensure_permission_to_change_user, only: %i[ update destroy ]
def index
set_page_and_extract_portion_from Current.account.users.active.alphabetically
end
def show
end
@@ -10,15 +14,25 @@ class UsersController < ApplicationController
def update
if @user.update(user_params)
redirect_to @user
respond_to do |format|
format.html { redirect_to @user }
format.json { head :no_content }
end
else
render :edit, status: :unprocessable_entity
respond_to do |format|
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @user.errors, status: :unprocessable_entity }
end
end
end
def destroy
@user.deactivate
redirect_to users_path
respond_to do |format|
format.html { redirect_to users_path }
format.json { head :no_content }
end
end
private
+14 -5
View File
@@ -1,6 +1,11 @@
module HtmlHelper
include ERB::Util
EXCLUDE_PUNCTUATION = %(.?,:!;"'<>)
EXCLUDE_PUNCTUATION_REGEX = /[#{Regexp.escape(EXCLUDE_PUNCTUATION)}]+\z/
def format_html(html)
fragment = Nokogiri::HTML.fragment(html)
fragment = Nokogiri::HTML5.fragment(html)
auto_link(fragment)
@@ -16,14 +21,16 @@ module HtmlHelper
fragment.traverse do |node|
next unless auto_linkable_node?(node)
content = node.text
# Take care to escape the html text node, so that the subsequent Nokogiri re-parse doesn't
# create tags where there aren't any.
content = h(node.text)
linked_content = content.dup
auto_link_urls(linked_content)
auto_link_emails(linked_content)
if linked_content != content
node.replace(Nokogiri::HTML.fragment(linked_content))
node.replace(Nokogiri::HTML5.fragment(linked_content))
end
end
end
@@ -40,8 +47,10 @@ module HtmlHelper
end
def extract_url_and_punctuation(url_match)
if url_match.end_with?(".", "?", ",", ":")
[ url_match[..-2], url_match[-1] ]
url_match = CGI.unescapeHTML(url_match)
if match = url_match.match(EXCLUDE_PUNCTUATION_REGEX)
len = match[0].length
[ url_match[..-(len+1)], url_match[-len..] ]
else
[ url_match, "" ]
end
-2
View File
@@ -10,8 +10,6 @@ class Account < ApplicationRecord
has_many :columns, dependent: :destroy
has_many :exports, class_name: "Account::Export", dependent: :destroy
has_many_attached :uploads
before_create :assign_external_account_id
after_create :create_join_code
+3
View File
@@ -1,8 +1,11 @@
class Account::JoinCode < ApplicationRecord
CODE_LENGTH = 12
USAGE_LIMIT_MAX = 10_000_000_000
belongs_to :account
validates :usage_limit, numericality: { less_than_or_equal_to: USAGE_LIMIT_MAX, message: "cannot be larger than the population of the planet" }
scope :active, -> { where("usage_count < usage_limit") }
before_create :generate_code, if: -> { code.blank? }
+39 -9
View File
@@ -1,4 +1,6 @@
class Card::Eventable::SystemCommenter
include ERB::Util
attr_reader :card, :event
def initialize(card, event)
@@ -15,25 +17,53 @@ class Card::Eventable::SystemCommenter
def comment_body
case event.action
when "card_assigned"
"#{event.creator.name} <strong>assigned</strong> this to #{event.assignees.pluck(:name).to_sentence}."
"#{creator_name} <strong>assigned</strong> this to #{assignee_names}."
when "card_unassigned"
"#{event.creator.name} <strong>unassigned</strong> from #{event.assignees.pluck(:name).to_sentence}."
"#{creator_name} <strong>unassigned</strong> from #{assignee_names}."
when "card_closed"
"<strong>Moved</strong> to “Done” by #{event.creator.name}"
"<strong>Moved</strong> to “Done” by #{creator_name}"
when "card_reopened"
"<strong>Reopened</strong> by #{event.creator.name}"
"<strong>Reopened</strong> by #{creator_name}"
when "card_postponed"
"#{event.creator.name} <strong>moved</strong> this to “Not Now”"
"#{creator_name} <strong>moved</strong> this to “Not Now”"
when "card_auto_postponed"
"<strong>Moved</strong> to “Not Now” due to inactivity"
when "card_title_changed"
"#{event.creator.name} <strong>changed the title</strong> from “#{event.particulars.dig('particulars', 'old_title')}” to “#{event.particulars.dig('particulars', 'new_title')}”."
"#{creator_name} <strong>changed the title</strong> from “#{old_title}” to “#{new_title}”."
when "card_board_changed"
"#{event.creator.name} <strong>moved</strong> this from “#{event.particulars.dig('particulars', 'old_board')}” to “#{event.particulars.dig('particulars', 'new_board')}”."
"#{creator_name} <strong>moved</strong> this from “#{old_board}” to “#{new_board}”."
when "card_triaged"
"#{event.creator.name} <strong>moved</strong> this to “#{event.particulars.dig('particulars', 'column')}"
"#{creator_name} <strong>moved</strong> this to “#{column}"
when "card_sent_back_to_triage"
"#{event.creator.name} <strong>moved</strong> this back to “Maybe?”"
"#{creator_name} <strong>moved</strong> this back to “Maybe?”"
end
end
def creator_name
h event.creator.name
end
def assignee_names
h event.assignees.pluck(:name).to_sentence
end
def old_title
h event.particulars.dig("particulars", "old_title")
end
def new_title
h event.particulars.dig("particulars", "new_title")
end
def old_board
h event.particulars.dig("particulars", "old_board")
end
def new_board
h event.particulars.dig("particulars", "new_board")
end
def column
h event.particulars.dig("particulars", "column")
end
end
+10 -4
View File
@@ -1,13 +1,19 @@
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account
attribute :session, :user, :identity, :account
attribute :http_method, :request_id, :user_agent, :ip_address, :referrer
delegate :identity, to: :session, allow_nil: true
def session=(value)
super(value)
if value.present? && account.present?
if value.present?
self.identity = session.identity
end
end
def identity=(identity)
super(identity)
if identity.present?
self.user = identity.users.find_by(account: account)
end
end
+7
View File
@@ -1,6 +1,7 @@
class Identity < ApplicationRecord
include Joinable, Transferable
has_many :access_tokens, dependent: :destroy
has_many :magic_links, dependent: :destroy
has_many :sessions, dependent: :destroy
has_many :users, dependent: :nullify
@@ -13,6 +14,12 @@ class Identity < ApplicationRecord
validates :email_address, format: { with: URI::MailTo::EMAIL_REGEXP }
normalizes :email_address, with: ->(value) { value.strip.downcase.presence }
def self.find_by_permissable_access_token(token, method:)
if (access_token = AccessToken.find_by(token: token)) && access_token.allows?(method)
access_token.identity
end
end
def send_magic_link(**attributes)
attributes[:purpose] = attributes.delete(:for) if attributes.key?(:for)
+10
View File
@@ -0,0 +1,10 @@
class Identity::AccessToken < ApplicationRecord
belongs_to :identity
has_secure_token
enum :permission, %w[ read write ].index_by(&:itself), default: :read
def allows?(method)
method.in?(%w[ GET HEAD ]) || write?
end
end
+1
View File
@@ -1,5 +1,6 @@
class Push::Subscription < ApplicationRecord
PERMITTED_ENDPOINT_HOSTS = %w[
jmt17.google.com
fcm.googleapis.com
updates.push.services.mozilla.com
web.push.apple.com
+18 -3
View File
@@ -1,7 +1,10 @@
class Webhook::Delivery < ApplicationRecord
class ResponseTooLarge < StandardError; end
STALE_TRESHOLD = 7.days
USER_AGENT = "fizzy/1.0.0 Webhook"
ENDPOINT_TIMEOUT = 7.seconds
MAX_RESPONSE_SIZE = 100.kilobytes
belongs_to :account, default: -> { webhook.account }
belongs_to :webhook
@@ -52,12 +55,16 @@ class Webhook::Delivery < ApplicationRecord
if resolved_ip.nil?
{ error: :private_uri }
else
response = http.request(
Net::HTTP::Post.new(uri, headers).tap { |request| request.body = payload }
)
request = Net::HTTP::Post.new(uri, headers).tap { |request| request.body = payload }
response = http.request(request) do |net_http_response|
stream_body_with_limit(net_http_response)
end
{ code: response.code.to_i }
end
rescue ResponseTooLarge
{ error: :response_too_large }
rescue Resolv::ResolvTimeout, Resolv::ResolvError, SocketError
{ error: :dns_lookup_failed }
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT
@@ -68,6 +75,14 @@ class Webhook::Delivery < ApplicationRecord
{ error: :failed_tls }
end
def stream_body_with_limit(response)
bytes_read = 0
response.read_body do |chunk|
bytes_read += chunk.bytesize
raise ResponseTooLarge if bytes_read > MAX_RESPONSE_SIZE
end
end
def resolved_ip
return @resolved_ip if defined?(@resolved_ip)
@resolved_ip = SsrfProtection.resolve_public_ip(uri.host)
+11 -2
View File
@@ -14,12 +14,21 @@
<%= form_with model: @join_code, url: account_join_code_path, method: :patch, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
<%= form.number_field :usage_limit,
required: true, autofocus: true, min: @join_code.usage_count,
required: true, autofocus: true,
in: 0..Account::JoinCode::USAGE_LIMIT_MAX,
class: "input center txt-large fit-content font-weight-black txt-align-center", style: "max-inline-size: 8ch",
data: { action: "keydown.esc@document->form#cancel focus->form#select" } %>
<% if @join_code.errors.any? %>
<div class="txt-negative txt-small">
<% @join_code.errors.full_messages.each do |message| %>
<p class="margin-block-none"><%= message %></p>
<% end %>
</div>
<% end %>
<p class="margin-none txt-subtle">
This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit %> times.
This code has been used <%= @join_code.usage_count %>/<%= @join_code.usage_limit_in_database %> times.
</p>
<%= form.button type: :submit, class: "btn btn--link center txt-medium", data: { form_target: "submit" } do %>
+2 -3
View File
@@ -1,8 +1,7 @@
json.cache! board do
json.(board, :id, :name, :all_access)
json.created_at board.created_at.utc
json.url board_url(board)
json.creator do
json.partial! "users/user", user: board.creator
end
json.creator board.creator, partial: "users/user", as: :user
end
@@ -0,0 +1 @@
json.array! @columns, partial: "columns/column", as: :column
@@ -0,0 +1 @@
json.partial! "columns/column", column: @column
+16 -16
View File
@@ -6,17 +6,17 @@
<% if board.published? %>
<div class="border-radius pad fill-selected">
<div class="flex-inline center justify-between gap">
<span class="txt-large"><%= icon_tag "lock" %></span>
<label class="switch flex align-center justify-between">
<%= form_with url: board_publication_path(board), method: :delete, data: { controller: "form" } do |form| %>
<%= form_with url: board_publication_path(board), method: :delete, class: "flex-inline center justify-between gap", data: { controller: "form" } do |form| %>
<label class="flex gap cursor-pointer">
<span class="txt-large"><%= icon_tag "lock" %></span>
<span class="switch flex align-center justify-between">
<%= form.check_box :published, class: "switch__input", checked: true, data: { action: "change->form#submit" }, disabled: !Current.user.can_administer_board?(@board) %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Turn off the public link</span>
<% end %>
</span>
<span class="for-screen-reader">Turn off the public link</span>
</label>
<span class="txt-large"><%= icon_tag "world" %></span>
</div>
<span class="txt-large" aria-hidden="true"><%= icon_tag "world" %></span>
<% end %>
<div class="flex align-center gap-half margin-block">
<%= text_field_tag :publication_url, published_board_url(board), readonly: true, class: "full-width input fill-white" %>
@@ -40,17 +40,17 @@
</div>
<% else %>
<div class="border-radius pad fill-shade">
<div class="flex-inline center justify-between gap">
<span class="txt-large"><%= icon_tag "lock" %></span>
<label class="switch flex align-center justify-between">
<%= form_with url: board_publication_path(board), method: :post, data: { controller: "form" } do |form| %>
<%= form_with url: board_publication_path(board), method: :post, class: "flex-inline center justify-between gap", data: { controller: "form" } do |form| %>
<span class="txt-large" aria-hidden="true"><%= icon_tag "lock" %></span>
<label class="flex gap cursor-pointer">
<span class="switch flex align-center justify-between">
<%= form.check_box :published, class: "switch__input", checked: false, data: { action: "change->form#submit" }, disabled: !Current.user.can_administer_board?(@board) %>
<span class="switch__btn round"></span>
<span class="for-screen-reader">Turn on the public link</span>
<% end %>
</span>
<span class="txt-large"><%= icon_tag "world" %></span>
<span class="for-screen-reader">Turn on the public link</span>
</label>
<span class="txt-large"><%= icon_tag "world" %></span>
</div>
<% end %>
</div>
<% end %>
<% end %>
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "boards/board", as: :board
+1
View File
@@ -0,0 +1 @@
json.partial! "boards/board", board: @board
+10 -16
View File
@@ -1,26 +1,20 @@
json.cache! [ card, card.column&.color ] do
json.(card, :id, :title, :status)
json.cache! card do
json.(card, :id, :number, :title, :status)
json.description card.description.to_plain_text
json.description_html card.description.to_s
json.image_url card.image.presence && url_for(card.image)
json.tags card.tags.pluck(:title).sort
json.golden card.golden?
json.last_active_at card.last_active_at.utc
json.created_at card.created_at.utc
json.url card_url(card)
json.board do
json.partial! "boards/board", locals: { board: card.board }
end
json.board card.board, partial: "boards/board", as: :board
json.column card.column, partial: "columns/column", as: :column if card.column
json.creator card.creator, partial: "users/user", as: :user
json.column do
if card.column
json.partial! "columns/column", column: card.column
else
nil
end
end
json.creator do
json.partial! "users/user", user: card.creator
end
json.comments_url card_comments_url(card)
end
@@ -9,9 +9,7 @@ json.cache! comment do
json.html comment.body.to_s
end
json.creator do
json.partial! "users/user", user: comment.creator
end
json.creator comment.creator, partial: "users/user", as: :user
json.reactions_url card_comment_reactions_url(comment.card_id, comment.id)
json.url card_comment_url(comment.card_id, comment.id)
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/comments/comment", as: :comment
@@ -0,0 +1,5 @@
json.cache! reaction do
json.(reaction, :id, :content)
json.reacter reaction.reacter, partial: "users/user", as: :user
json.url card_comment_reaction_url(reaction.comment.card, reaction.comment, reaction)
end
@@ -0,0 +1 @@
json.array! @comment.reactions.ordered, partial: "cards/comments/reactions/reaction", as: :reaction
@@ -0,0 +1 @@
json.partial! "cards/comments/comment", comment: @comment
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "cards/card", as: :card
+2
View File
@@ -0,0 +1,2 @@
json.partial! "cards/card", card: @card
json.steps @card.steps, partial: "cards/steps/step", as: :step
@@ -0,0 +1,3 @@
json.cache! step do
json.(step, :id, :content, :completed)
end
+1
View File
@@ -0,0 +1 @@
json.partial! "cards/steps/step", step: @step
+4 -2
View File
@@ -1,2 +1,4 @@
json.(column, :id, :name, :color)
json.created_at column.created_at.utc
json.cache! column do
json.(column, :id, :name, :color)
json.created_at column.created_at.utc
end
@@ -0,0 +1,13 @@
<tr style="view-transition-name: <%= dom_id(access_token) %>">
<td><strong><%= access_token.description %></strong></td>
<td><%= access_token.permission.humanize %></td>
<td><%= local_datetime_tag access_token.created_at, style: :datetime %></td>
<td>
<%= button_to my_access_token_path(access_token), method: :delete,
class: "btn txt-negative btn--circle txt-x-small borderless fill-transparent",
data: { turbo_confirm: "Are you sure you want to permanently revoke this access token?" } do %>
<%= icon_tag "trash" %>
<span class="for-screen-reader">Edit this token</span>
<% end %>
</td>
</tr>
+35
View File
@@ -0,0 +1,35 @@
<% @page_title = "Personal access tokens" %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to "My profile", user_path(Current.user), "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<section class="panel panel--wide shadow center webhooks">
<% if @access_tokens.any? %>
<p class="margin-none-block-start">Tokens you have generated that can be used to access the Fizzy API.</p>
<table class="access_tokens_table margin-block-end-double max-width txt-small">
<thead>
<tr>
<th>Description</th>
<th>Permission</th>
<th>Created</th>
<th></th>
</tr>
</thead>
<tbody>
<%= render partial: "my/access_tokens/access_token", collection: @access_tokens %>
</tbody>
</table>
<% else %>
<p class="margin-none-block-start">Personal access tokens can be used like a password to access the Fizzy developer API. You can have as many tokens as you need and revoke access to each one at any time.</p>
<% end %>
<%= link_to new_my_access_token_path, class: "btn btn--link" do %>
<%= icon_tag "add" %>
<span>Generate a new access token</span>
<% end %>
</section>
+29
View File
@@ -0,0 +1,29 @@
<% @page_title = "Generate a personal access token" %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to "tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
<%= form_with model: @access_token, url: my_access_tokens_path, scope: :access_token, data: { controller: "form" }, html: { class: "flex flex-column gap" } do |form| %>
<div class="flex flex-column gap-half">
<strong><%= form.label :description, "Access token description" %></strong>
<%= form.text_field :description, required: true, autofocus: true, class: "input", placeholder: "e.g. Github", data: { action: "keydown.esc@document->form#cancel" } %>
</div>
<div class="flex flex-column gap-half">
<strong><%= form.label :permission %></strong>
<%= form.select :permission, options_for_select({ "Read" => "read", "Read + Write" => "write"}), {}, class: "input input--select" %>
</div>
<%= form.button type: :submit, class: "btn btn--link center txt-medium" do %>
<span>Generate access token</span>
<% end %>
<%= link_to "Cancel and go back", my_access_tokens_path, data: { form_target: "cancel" }, hidden: true %>
<% end %>
</article>
+26
View File
@@ -0,0 +1,26 @@
<% @page_title = "New personal access token" %>
<% content_for :header do %>
<div class="header__actions header__actions--start">
<%= back_link_to "Tokens", my_access_tokens_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
</div>
<h1 class="header__title"><%= @page_title %></h1>
<% end %>
<article class="panel panel--wide shadow center txt-align-start" style="view-transition-name: <%= dom_id(@access_token) %>">
<div class="flex flex-column gap">
<label class="flex flex-column gap-half txt-align-start">
<strong><%= @access_token.description %> (<%= @access_token.permission == "write" ? "Read + Write" : "Read" %>)</strong>
<input type="text" value="<%= @access_token.token %>" class="input" readonly>
</label>
<p class="margin-none txt-small">Be sure to save this access token now because you wont be able to see it again.</p>
<%= tag.button class: "btn btn--link center", data: {
controller: "copy-to-clipboard", action: "copy-to-clipboard#copy",
copy_to_clipboard_success_class: "btn--success", copy_to_clipboard_content_value: @access_token.token } do %>
<%= icon_tag "copy-paste" %>
<span>Copy access token</span>
<% end %>
</div>
</article>
@@ -0,0 +1,4 @@
json.cache! account do
json.(account, :id, :name, :slug)
json.created_at account.created_at.utc
end
@@ -0,0 +1,4 @@
json.accounts @identity.users do |user|
json.partial! "my/identities/account", account: user.account
json.user user, partial: "users/user", as: :user
end
+4
View File
@@ -25,4 +25,8 @@
</div>
<%= yield %>
<div class="nav__blank-slate blank-slate blank-slate--empty">
Nothing matches that filter
</div>
</div>
@@ -0,0 +1,17 @@
json.cache! notification do
json.(notification, :id)
json.read notification.read?
json.read_at notification.read_at&.utc
json.created_at notification.created_at.utc
json.partial! "notifications/notification/#{notification.source_type.underscore}/body", notification: notification
json.creator notification.creator, partial: "users/user", as: :user
json.card do
json.(notification.card, :id, :title, :status)
json.url card_url(notification.card)
end
json.url notification_url(notification)
end
@@ -0,0 +1 @@
json.array! (@unread || []) + @page.records, partial: "notifications/notification", as: :notification
@@ -0,0 +1,2 @@
json.title event_notification_title(notification.source)
json.body event_notification_body(notification.source)
@@ -0,0 +1,4 @@
mention = notification.source
json.title "#{mention.mentioner.first_name} @mentioned you"
json.body mention.source.mentionable_content.truncate(200)
+5
View File
@@ -0,0 +1,5 @@
json.cache! tag do
json.(tag, :id, :title)
json.created_at tag.created_at.utc
json.url cards_url(tag_ids: [ tag ])
end
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "tags/tag", as: :tag
-4
View File
@@ -1,4 +0,0 @@
json.message "File uploaded successfully"
json.fileName @upload.filename.to_s
json.mimetype @upload.content_type
json.fileUrl @upload.slug_url
+6 -1
View File
@@ -1,6 +1,11 @@
<div class="flex flex-column gap align-center margin-block-double">
<% url = session_transfer_url(user.identity.transfer_id, script_name: nil) %>
<header class="full-width">
<h2 class="divider txt-large">Link a device</h2>
<p class="margin-none-block" id="session_transfer_label">Use this link to sign-in on another device</p>
</header>
<label class="flex flex-column gap full-width">
<header>
<strong class="divider txt-large">Devices</strong>
@@ -35,4 +40,4 @@
<span class="for-screen-reader">Copy auto-login link</span>
<% end %>
</div>
</div>
</div>
+1 -1
View File
@@ -1,7 +1,7 @@
json.cache! user do
json.(user, :id, :name, :role, :active)
json.email_address user.identity.email_address
json.email_address user.identity&.email_address
json.created_at user.created_at.utc
json.url user_url(user)
+1
View File
@@ -0,0 +1 @@
json.array! @page.records, partial: "users/user", as: :user
+15 -2
View File
@@ -32,9 +32,9 @@
<% if @user.verified? %>
<div class="flex-inline center justify-center flex-wrap gap">
<%= link_to "Which cards are assigned to #{me_or_you}?",
cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
cards_path(assignee_ids: [ @user.id ], sorted_by: "newest"), class: "btn btn--link", data: { turbo_frame: "_top" } %>
<%= link_to "Which cards were added by #{me_or_you}?",
cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn", data: { turbo_frame: "_top" } %>
cards_path(creator_ids: [ @user.id ], sorted_by: "newest"), class: "btn btn--link", data: { turbo_frame: "_top" } %>
</div>
<% end %>
</div>
@@ -49,6 +49,19 @@
<%= button_to session_url(script_name: nil), method: :delete, class: "btn txt-x-small center", data: { turbo: false } do %>
<span>Sign out of Fizzy on this device</span>
<div class="flex flex-column align-center gap margin-block-start-double">
<header class="full-width">
<h2 class="divider txt-large margin-none-block">API</h2>
</header>
<div class="flex align-center gap txt-normal">
<%= link_to "Personal access tokens", my_access_tokens_path, class: "btn" %>
</div>
</div>
<div class="center margin-block-start-double">
<%= button_to session_url(script_name: nil), method: :delete, class: "btn btn--plain txt-link txt-small", data: { turbo: false } do %>
<span>Sign out of Fizzy</span>
<% end %>
</section>
<% end %>
+1
View File
@@ -0,0 +1 @@
json.partial! "users/user", user: @user
+1 -1
View File
@@ -15,7 +15,7 @@
<div class="flex flex-column gap-half">
<%= form.label :actions do %>
<strong>Events</strong><br>
<p class="margin-none txt-x-small txt-subtle">Trigger a call to the Payload URL when these things occur:</p>
<p class="margin-none txt-x-small txt-subtle">Trigger a call to the Payload URL when:</p>
<% end %>
<%= render "webhooks/form/actions", form: form %>
</div>
+2 -7
View File
@@ -9,11 +9,6 @@ json.cache! @event do
end
end
json.board do
json.partial! "boards/board", locals: { board: @event.board }
end
json.creator do
json.partial! "users/user", user: @event.creator
end
json.board @event.board, partial: "boards/board", as: :board
json.creator @event.creator, partial: "users/user", as: :user
end
+7 -6
View File
@@ -4,13 +4,14 @@
webhook_action_options,
:first,
:last do |item| %>
<li class="list-style-none margin-none flex align-center gap">
<label class="switch toggler__visible-when-off flex-item-no-shrink txt-x-small">
<%= item.check_box(class: "switch__input") %>
<span class="switch__btn round"></span>
<span class="for-screen-reader"><%= item.text %></span>
<li class="list-style-none margin-none">
<label class="toggler__visible-when-off flex align-center gap cursor-pointer">
<span class="switch txt-x-small flex-item-no-shrink">
<%= item.check_box(class: "switch__input") %>
<span class="switch__btn round"></span>
</span>
<span class="min-width"><%= item.text %></span>
</label>
<span><%= item.text %></span>
</li>
<% end %>
</ul>
+1 -1
View File
@@ -31,7 +31,7 @@
<div class="flex flex-column gap-half">
<%= form.label :actions do %>
<strong>Events</strong><br>
<p class="margin-none txt-x-small txt-subtle">Trigger a call to the Payload URL when these things occur:</p>
<p class="margin-none txt-x-small txt-subtle">Trigger a call to the Payload URL when:</p>
<% end %>
<%= render "webhooks/form/actions", form: form %>
</div>
+2 -2
View File
@@ -4,8 +4,8 @@ bin/rails runner - <<EOF
puts "Login with david@example.com to: http://fizzy.localhost:3006/"
EOF
if [ -f tmp/solid-queue.txt ]; then
export SOLID_QUEUE_IN_PUMA=1
if [ ! -f tmp/solid-queue.txt ]; then
export SOLID_QUEUE_IN_PUMA=false
fi
if [ -f tmp/oss-config.txt ]; then
+2
View File
@@ -19,6 +19,8 @@ CI.run do
if Fizzy.saas?
step "Tests: SaaS", "#{SAAS_ENV} bin/rails test"
step "Tests: SaaS System", "#{SAAS_ENV} #{SYSTEM_TEST_ENV} bin/rails test:system"
step "Tests: OSS", "#{OSS_ENV} bin/rails test"
step "Tests: OSS System", "#{OSS_ENV} #{SYSTEM_TEST_ENV} bin/rails test:system"
else
step "Tests: SQLite", "#{OSS_ENV} bin/rails test"
step "Tests: SQLite System", "#{OSS_ENV} #{SYSTEM_TEST_ENV} bin/rails test:system"
+2 -1
View File
@@ -5,6 +5,7 @@ default: &default
username: <%= ENV.fetch("MYSQL_USER", "root") %>
password: <%= ENV["MYSQL_PASSWORD"] %>
pool: 50
ssl_mode: <%= ENV["MYSQL_SSL_MODE"] %>
timeout: 5000
development:
@@ -40,4 +41,4 @@ production:
cache:
<<: *default
database: fizzy_production_cache
migrations_paths: db/cache_migrate
migrations_paths: db/cache_migrate
+2 -1
View File
@@ -28,7 +28,8 @@ env:
- SMTP_USERNAME
- SMTP_PASSWORD
clear:
MAILER_FROM_ADDRESS: support@example.com # The email address that Fizzy sends email from
MAILER_FROM_ADDRESS: support@example.com # The email "from" address that Fizzy sends email from
SMTP_ADDRESS: mail.example.com # The SMTP server you'll use to send email
SOLID_QUEUE_IN_PUMA: true # Run background jobs in the app container
+22 -14
View File
@@ -5,18 +5,20 @@ Rails.application.configure do
# Email provider Settings
#
# Configure these according to whichever email provider you use. An example setup
# using SMTP looks like the following:
#
# config.action_mailer.smtp_settings = {
# address: 'smtp.example.com', # The address of your email provider's SMTP server
# port: 2525,
# domain: 'example.com', # Your domain, which Fizzy will send email from
# user_name: ENV["SMTP_USERNAME"],
# password: ENV["SMTP_PASSWORD"],
# authentication: :plain,
# enable_starttls_auto: true
# }
# SMTP setting can be configured via environment variables.
# For other configuration options, consult the Action Mailer documentation.
if smtp_address = ENV["SMTP_ADDRESS"].presence
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: smtp_address,
port: ENV.fetch("SMTP_PORT", "587").to_i,
domain: ENV.fetch("SMTP_DOMAIN", nil),
user_name: ENV.fetch("SMTP_USERNAME", nil),
password: ENV.fetch("SMTP_PASSWORD", nil),
authentication: ENV.fetch("SMTP_AUTHENTICATION", "plain"),
enable_starttls_auto: ENV.fetch("SMTP_ENABLE_STARTTLS_AUTO", "true") == "true"
}
end
# Code is not reloaded between requests.
config.enable_reloading = false
@@ -40,6 +42,12 @@ Rails.application.configure do
"Cache-Control" => "public, max-age=#{1.year.to_i}"
}
# Select Active Storage service via env var; default to local disk.
# Don't overwrite if it's already been set (e.g. by fizzy-saas)
if config.active_storage.service.blank?
config.active_storage.service = ENV.fetch("ACTIVE_STORAGE_SERVICE", "local").to_sym
end
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
@@ -54,10 +62,10 @@ Rails.application.configure do
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
# Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies.
config.assume_ssl = true
config.assume_ssl = ENV.fetch("ASSUME_SSL", "true") == "true"
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
config.force_ssl = ENV.fetch("FORCE_SSL", "true") == "true"
# Log to STDOUT by default
config.logger = ActiveSupport::Logger.new(STDOUT)
+3 -4
View File
@@ -11,10 +11,9 @@ end
# before the User model's upload callback, causing FileNotFoundError when
# using `process: :immediately` for variants.
# See: https://github.com/rails/rails/issues/53694
#
# ActiveSupport.on_load(:active_storage_record) do
# configure_replica_connections
# end
ActiveSupport.on_load(:active_storage_record) do
configure_replica_connections
end
module ActiveStorageControllerExtensions
extend ActiveSupport::Concern
+5 -2
View File
@@ -7,8 +7,11 @@ pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid")
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
# Run Solid Queue with Puma
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
# Run Solid Queue with Puma by default.
# Disabled when running fizzy-saas or via SOLID_QUEUE_IN_PUMA=false.
unless Fizzy.saas? || ENV["SOLID_QUEUE_IN_PUMA"] == "false"
plugin :solid_queue
end
# Expose Prometheus metrics at http://0.0.0.0:9394/metrics (SaaS only).
# In dev, overridden to http://127.0.0.1:9306/metrics in .mise.toml.
+4
View File
@@ -94,6 +94,8 @@ Rails.application.routes.draw do
end
end
resources :tags, only: :index
namespace :notifications do
resource :settings
resource :unsubscribe
@@ -162,6 +164,8 @@ Rails.application.routes.draw do
resource :landing
namespace :my do
resource :identity, only: :show
resources :access_tokens
resources :pins
resource :timezone
resource :menu
+11
View File
@@ -16,3 +16,14 @@ devminio:
region: us-east-1 # default region required for signer
access_key_id: minioadmin
secret_access_key: minioadmin
s3:
service: S3
access_key_id: <%= ENV["S3_ACCESS_KEY_ID"] %>
bucket: <%= ENV["S3_BUCKET"] || "fizzy-#{Rails.env}-activestorage" %>
endpoint: <%= ENV["S3_ENDPOINT"] %>
force_path_style: <%= ENV["S3_FORCE_PATH_STYLE"] == "true" %>
region: <%= ENV.fetch("S3_REGION", "us-east-1") %>
request_checksum_calculation: <%= ENV.fetch("S3_REQUEST_CHECKSUM_CALCULATION", "when_supported") %>
response_checksum_validation: <%= ENV.fetch("S3_RESPONSE_CHECKSUM_VALIDATION", "when_supported") %>
secret_access_key: <%= ENV["S3_SECRET_ACCESS_KEY"] %>

Some files were not shown because too many files have changed in this diff Show More