Merge branch 'main' into theme-switcher-revised
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -30,4 +30,3 @@ updates:
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
semver-major-days: 14
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -6,4 +6,6 @@ paths = [
|
||||
'''log''',
|
||||
'''tmp''',
|
||||
'''.*\.yml\.enc''',
|
||||
'''docs/''',
|
||||
'''test/''',
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,10 @@
|
||||
|
||||
max-height: 200px;
|
||||
overflow: scroll;
|
||||
|
||||
&:is(.lexxy-prompt-menu--visible) {
|
||||
padding: var(--lexxy-prompt-padding);
|
||||
}
|
||||
}
|
||||
|
||||
.lexxy-prompt-menu--visible {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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? }
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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
|
||||
@@ -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 %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "boards/board", as: :board
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "boards/board", board: @board
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "cards/card", as: :card
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "cards/steps/step", step: @step
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 won’t 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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "tags/tag", as: :tag
|
||||
@@ -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
|
||||
@@ -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,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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.array! @page.records, partial: "users/user", as: :user
|
||||
@@ -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 %>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
json.partial! "users/user", user: @user
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user