├── .env.example ├── .github └── workflows │ ├── main.yml │ └── semgrep.yml ├── .gitignore ├── .rspec ├── .ruby-version ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── apps ├── rack │ ├── app.rb │ ├── config.ru │ └── middleware │ │ └── disable_paths.rb ├── rails-api │ ├── .dockerignore │ ├── .gitattributes │ ├── .gitignore │ ├── .kamal │ │ ├── hooks │ │ │ ├── docker-setup.sample │ │ │ ├── post-deploy.sample │ │ │ ├── post-proxy-reboot.sample │ │ │ ├── pre-build.sample │ │ │ ├── pre-connect.sample │ │ │ ├── pre-deploy.sample │ │ │ └── pre-proxy-reboot.sample │ │ └── secrets │ ├── .rubocop.yml │ ├── .ruby-version │ ├── Dockerfile │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── app │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── home_controller.rb │ │ ├── jobs │ │ │ └── application_job.rb │ │ ├── mailers │ │ │ └── application_mailer.rb │ │ ├── models │ │ │ └── application_record.rb │ │ └── views │ │ │ └── layouts │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ ├── bin │ │ ├── brakeman │ │ ├── bundle │ │ ├── dev │ │ ├── docker-entrypoint │ │ ├── jobs │ │ ├── kamal │ │ ├── rails │ │ ├── rake │ │ ├── rubocop │ │ ├── setup │ │ └── thrust │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── cache.yml │ │ ├── credentials.yml.enc │ │ ├── database.yml │ │ ├── deploy.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── cors.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ └── inflections.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── puma.rb │ │ ├── queue.yml │ │ ├── recurring.yml │ │ ├── routes.rb │ │ └── storage.yml │ ├── db │ │ ├── cable_schema.rb │ │ ├── cache_schema.rb │ │ ├── queue_schema.rb │ │ └── seeds.rb │ ├── public │ │ └── robots.txt │ └── test │ │ ├── controllers │ │ └── home_controller_test.rb │ │ └── test_helper.rb ├── rails-full │ ├── .dockerignore │ ├── .gitattributes │ ├── .gitignore │ ├── .kamal │ │ ├── hooks │ │ │ ├── docker-setup.sample │ │ │ ├── post-deploy.sample │ │ │ ├── post-proxy-reboot.sample │ │ │ ├── pre-build.sample │ │ │ ├── pre-connect.sample │ │ │ ├── pre-deploy.sample │ │ │ └── pre-proxy-reboot.sample │ │ └── secrets │ ├── .rubocop.yml │ ├── .ruby-version │ ├── Dockerfile │ ├── Gemfile │ ├── Gemfile.lock │ ├── README.md │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ └── home_controller.rb │ │ ├── helpers │ │ │ ├── application_helper.rb │ │ │ └── home_helper.rb │ │ ├── javascript │ │ │ ├── application.js │ │ │ └── controllers │ │ │ │ ├── application.js │ │ │ │ ├── hello_controller.js │ │ │ │ └── index.js │ │ ├── jobs │ │ │ └── application_job.rb │ │ ├── mailers │ │ │ └── application_mailer.rb │ │ ├── models │ │ │ └── application_record.rb │ │ └── views │ │ │ ├── home │ │ │ └── index.html.erb │ │ │ ├── layouts │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ │ │ └── pwa │ │ │ ├── manifest.json.erb │ │ │ └── service-worker.js │ ├── bin │ │ ├── brakeman │ │ ├── bundle │ │ ├── dev │ │ ├── docker-entrypoint │ │ ├── importmap │ │ ├── jobs │ │ ├── kamal │ │ ├── rails │ │ ├── rake │ │ ├── rubocop │ │ ├── setup │ │ └── thrust │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── cache.yml │ │ ├── credentials.yml.enc │ │ ├── database.yml │ │ ├── deploy.yml │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── importmap.rb │ │ ├── initializers │ │ │ ├── assets.rb │ │ │ ├── clerk.rb │ │ │ ├── content_security_policy.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ └── inflections.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── puma.rb │ │ ├── queue.yml │ │ ├── recurring.yml │ │ ├── routes.rb │ │ └── storage.yml │ ├── db │ │ ├── cable_schema.rb │ │ ├── cache_schema.rb │ │ ├── queue_schema.rb │ │ └── seeds.rb │ ├── public │ │ ├── 400.html │ │ ├── 404.html │ │ ├── 406-unsupported-browser.html │ │ ├── 422.html │ │ ├── 500.html │ │ ├── icon.png │ │ ├── icon.svg │ │ └── robots.txt │ └── test │ │ ├── application_system_test_case.rb │ │ ├── controllers │ │ └── home_controller_test.rb │ │ └── test_helper.rb └── sinatra │ ├── app.rb │ ├── config.ru │ └── views │ └── index.erb ├── bin ├── console ├── release └── setup ├── clerk-sdk-ruby.gemspec ├── docs ├── clerk-logo-dark.png └── clerk-logo-light.png ├── lib ├── clerk.rb └── clerk │ ├── authenticatable.rb │ ├── authenticate_context.rb │ ├── authenticate_request.rb │ ├── configuration.rb │ ├── constants.rb │ ├── error.rb │ ├── jwks_cache.rb │ ├── proxy.rb │ ├── rack.rb │ ├── rack_middleware.rb │ ├── rails.rb │ ├── railtie.rb │ ├── sdk.rb │ ├── sinatra.rb │ ├── utils.rb │ └── version.rb └── spec ├── clerk ├── authenticatable_spec.rb ├── authenticate_context_spec.rb ├── authenticate_request_spec.rb ├── configuration_spec.rb ├── error_spec.rb ├── jwks_cache_spec.rb ├── proxy_spec.rb ├── rack_middleware_spec.rb ├── sdk_spec.rb └── utils_spec.rb ├── clerk_spec.rb └── spec_helper.rb /.env.example: -------------------------------------------------------------------------------- 1 | CLERK_JS_URL=https://YOUR_SUBDOMAIN.clerk.accounts.dev/npm/@clerk/clerk-js@latest/dist/clerk.browser.js 2 | CLERK_PUBLISHABLE_KEY=pk_test_****** 3 | CLERK_SECRET_KEY=sk_test_****** -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | name: Ruby ${{ matrix.ruby }} 13 | strategy: 14 | matrix: 15 | ruby: 16 | - "3.3.5" 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | env: 24 | BUNDLE_FROZEN: false 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | bundler-cache: true 28 | 29 | - name: Unit Tests 30 | run: bundle exec rake spec 31 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | on: 3 | workflow_dispatch: {} 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - .github/workflows/semgrep.yml 10 | schedule: 11 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 12 | - cron: '15 18 * * *' 13 | jobs: 14 | semgrep: 15 | name: semgrep/ci 16 | runs-on: ubuntu-22.04 17 | env: 18 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 19 | container: 20 | image: returntocorp/semgrep 21 | if: (github.actor != 'dependabot[bot]') 22 | steps: 23 | - uses: actions/checkout@v3 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | .byebug_history 11 | *.gem 12 | 13 | .idea 14 | 15 | # rspec failure tracking 16 | .rspec_status 17 | 18 | # Environment variables 19 | .env 20 | .env.* 21 | !.env.example 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in clerk.gemspec 6 | gemspec 7 | 8 | group :development do 9 | gem "activesupport", "~> 8.0", require: false 10 | gem "dotenv", "~> 3.1", require: false 11 | gem "puma", "~> 6.4", ">= 6.4.3", require: false 12 | gem "rack", "~> 3.1", ">= 3.1.14", require: false 13 | gem "rbs", "~> 3.6" 14 | gem "rake", "~> 13.0" 15 | gem "rackup", "~> 2.2", require: false 16 | gem "rb-fsevent", "~> 0.11", ">= 0.11.2", platform: [:ruby], require: false 17 | gem "rerun", "~> 0.14", require: false 18 | gem "sinatra", "~> 4.1", ">= 4.1.1", require: false 19 | gem "standard", "~> 1.42" 20 | gem "wdm", "~> 0.2", platform: [:mswin, :windows] 21 | end 22 | 23 | group :development, :test do 24 | gem "rspec", "~> 3.0" 25 | gem "rspec-rails", "~> 7.1" 26 | gem "guard-rspec", "~> 4.7", require: false 27 | gem "guard-rake", "~> 1.0", require: false 28 | end 29 | 30 | group :test do 31 | gem "simplecov", require: false 32 | gem "climate_control", require: false 33 | end 34 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :rspec, cmd: "bundle exec rspec", group: :spec do 2 | require "guard/rspec/dsl" 3 | dsl = Guard::RSpec::Dsl.new(self) 4 | 5 | # RSpec files 6 | rspec = dsl.rspec 7 | watch(rspec.spec_helper) { rspec.spec_dir } 8 | watch(rspec.spec_support) { rspec.spec_dir } 9 | watch(rspec.spec_files) 10 | 11 | # Ruby files 12 | ruby = dsl.ruby 13 | dsl.watch_spec_files_for(ruby.lib_files) 14 | end 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Clerk Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "standard/rake" 5 | 6 | ################################ 7 | # COVERAGE 8 | ################################ 9 | 10 | desc "Open coverage report" 11 | task(:cov) { system "open coverage/index.html" } 12 | 13 | ################################ 14 | # TESTING 15 | ################################ 16 | 17 | desc "Run all tests" 18 | task spec: "spec:all" 19 | 20 | namespace :spec do 21 | task(:all) { system "bundle exec rspec" } 22 | 23 | desc "Run tests on file changes" 24 | task(:watch) { system "bundle exec guard -g spec" } 25 | 26 | desc "Run failed tests only" 27 | task(:failed) { system "bundle exec rspec --only-failures" } 28 | end 29 | 30 | ################################ 31 | # PLAYGROUND APPLICATIONS 32 | ################################ 33 | 34 | namespace :app do 35 | desc "Run Rails (full stack) application" 36 | task rails: "rails:full" 37 | 38 | namespace :rails do 39 | task :full do 40 | cd ("apps/rails-full") { system "bin/rails server" } 41 | end 42 | 43 | desc "Run Rails (API only) application" 44 | task :api do 45 | cd ("apps/rails-api") { system "bin/rails server" } 46 | end 47 | end 48 | 49 | desc "Run Rack application" 50 | task(:rack) { system "rerun --dir lib,apps/rack --pattern '**/*.{rb,ru}' -- bundle exec puma apps/rack/config.ru -p 3000" } 51 | 52 | desc "Run Sinatra application" 53 | task(:sinatra) { system "rerun --dir lib,apps/sinatra --pattern '**/*.{erb,rb,ru}' -- bundle exec puma apps/sinatra/config.ru -p 3000 -v" } 54 | end 55 | 56 | 57 | -------------------------------------------------------------------------------- /apps/rack/app.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | require "clerk" 3 | 4 | class App 5 | def call(env) 6 | # # Example: Without using `Clerk::Rack::Reverification` Middleware 7 | # preset = Clerk::StepUp::Preset::LAX 8 | # if env["clerk"].user_needs_reverification?(preset) 9 | # return env["clerk"].user_reverification_rack_response(preset) 10 | # end 11 | 12 | respond_with(200) do 13 | user = env["clerk"].user 14 | user ? "Authenticated User: #{user.first_name} (#{user.id})" : "Not Authenticated" 15 | end 16 | end 17 | 18 | private 19 | 20 | def respond_with(status, plain_body = nil, &html_body) 21 | return [status, {"Content-Type" => "text/plain; charset=utf-8"}, [plain_body]] unless block_given? 22 | 23 | compiled = <<-HTML 24 | 25 | 26 | Rack 27 | 36 | 43 | 56 | 57 | 58 |

Rack

59 |

#{yield}

60 |
61 | 62 | 63 | HTML 64 | 65 | [status, {"Content-Type" => "text/html; charset=utf-8"}, [compiled]] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /apps/rack/config.ru: -------------------------------------------------------------------------------- 1 | require "active_support" 2 | require "rack" 3 | require "clerk/rack" 4 | require "dotenv" 5 | 6 | require_relative "app" 7 | require_relative "middleware/disable_paths" 8 | 9 | Dotenv.load(".env") 10 | 11 | use DisablePaths, paths: ["/favicon.ico"] 12 | use Clerk::Rack::Middleware 13 | use Clerk::Rack::Reverification, 14 | preset: Clerk::StepUp::Preset::LAX, 15 | routes: ["/*"] 16 | 17 | run App.new 18 | -------------------------------------------------------------------------------- /apps/rack/middleware/disable_paths.rb: -------------------------------------------------------------------------------- 1 | class DisablePaths 2 | def initialize(app, paths: []) 3 | @app = app 4 | @paths = {} 5 | 6 | paths.each { |p| @paths[p] = true } 7 | end 8 | 9 | def call(env) 10 | return [404, {}, []] if @paths[env["PATH_INFO"]] 11 | @app.call(env) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /apps/rails-api/.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore CI service files. 34 | /.github 35 | 36 | # Ignore development files 37 | /.devcontainer 38 | 39 | # Ignore Docker-related files 40 | /.dockerignore 41 | /Dockerfile* 42 | -------------------------------------------------------------------------------- /apps/rails-api/.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /apps/rails-api/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | # Ignore master key for decrypting credentials and more. 32 | /config/master.key 33 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLE (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLE (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/pre-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-deploy hook 4 | # 5 | # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 | # 7 | # Fails unless the combined status is "success" 8 | # 9 | # These environment variables are available: 10 | # KAMAL_RECORDED_AT 11 | # KAMAL_PERFORMER 12 | # KAMAL_VERSION 13 | # KAMAL_HOSTS 14 | # KAMAL_COMMAND 15 | # KAMAL_SUBCOMMAND 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | # Only check the build status for production deployments 20 | if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 | exit 0 22 | end 23 | 24 | require "bundler/inline" 25 | 26 | # true = install gems so this is fast on repeat invocations 27 | gemfile(true, quiet: true) do 28 | source "https://rubygems.org" 29 | 30 | gem "octokit" 31 | gem "faraday-retry" 32 | end 33 | 34 | MAX_ATTEMPTS = 72 35 | ATTEMPTS_GAP = 10 36 | 37 | def exit_with_error(message) 38 | $stderr.puts message 39 | exit 1 40 | end 41 | 42 | class GithubStatusChecks 43 | attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 | 45 | def initialize 46 | @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") 47 | @git_sha = `git rev-parse HEAD`.strip 48 | @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 | refresh! 50 | end 51 | 52 | def refresh! 53 | @combined_status = github_client.combined_status(remote_url, git_sha) 54 | end 55 | 56 | def state 57 | combined_status[:state] 58 | end 59 | 60 | def first_status_url 61 | first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 | first_status && first_status[:target_url] 63 | end 64 | 65 | def complete_count 66 | combined_status[:statuses].count { |status| status[:state] != "pending"} 67 | end 68 | 69 | def total_count 70 | combined_status[:statuses].count 71 | end 72 | 73 | def current_status 74 | if total_count > 0 75 | "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 | else 77 | "Build not started..." 78 | end 79 | end 80 | end 81 | 82 | 83 | $stdout.sync = true 84 | 85 | puts "Checking build status..." 86 | attempts = 0 87 | checks = GithubStatusChecks.new 88 | 89 | begin 90 | loop do 91 | case checks.state 92 | when "success" 93 | puts "Checks passed, see #{checks.first_status_url}" 94 | exit 0 95 | when "failure" 96 | exit_with_error "Checks failed, see #{checks.first_status_url}" 97 | when "pending" 98 | attempts += 1 99 | end 100 | 101 | exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 102 | 103 | puts checks.current_status 104 | sleep(ATTEMPTS_GAP) 105 | checks.refresh! 106 | end 107 | rescue Octokit::NotFound 108 | exit_with_error "Build status could not be found" 109 | end 110 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /apps/rails-api/.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Example of extracting secrets from 1password (or another compatible pw manager) 6 | # SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 7 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) 8 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) 9 | 10 | # Use a GITHUB_TOKEN if private repositories are needed for the image 11 | # GITHUB_TOKEN=$(gh config get -h github.com oauth_token) 12 | 13 | # Grab the registry password from ENV 14 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 15 | 16 | # Improve security by using a password manager. Never check config/master.key into git! 17 | RAILS_MASTER_KEY=$(cat config/master.key) 18 | -------------------------------------------------------------------------------- /apps/rails-api/.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | -------------------------------------------------------------------------------- /apps/rails-api/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /apps/rails-api/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: 5 | # docker build -t rails_api . 6 | # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name rails_api rails_api 7 | 8 | # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html 9 | 10 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version 11 | ARG RUBY_VERSION=3.3.5 12 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base 13 | 14 | # Rails app lives here 15 | WORKDIR /rails 16 | 17 | # Install base packages 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ 20 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 21 | 22 | # Set production environment 23 | ENV RAILS_ENV="production" \ 24 | BUNDLE_DEPLOYMENT="1" \ 25 | BUNDLE_PATH="/usr/local/bundle" \ 26 | BUNDLE_WITHOUT="development" 27 | 28 | # Throw-away build stage to reduce size of final image 29 | FROM base AS build 30 | 31 | # Install packages needed to build gems 32 | RUN apt-get update -qq && \ 33 | apt-get install --no-install-recommends -y build-essential git pkg-config && \ 34 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 35 | 36 | # Install application gems 37 | COPY Gemfile Gemfile.lock ./ 38 | RUN bundle install && \ 39 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ 40 | bundle exec bootsnap precompile --gemfile 41 | 42 | # Copy application code 43 | COPY . . 44 | 45 | # Precompile bootsnap code for faster boot times 46 | RUN bundle exec bootsnap precompile app/ lib/ 47 | 48 | 49 | 50 | 51 | # Final stage for app image 52 | FROM base 53 | 54 | # Copy built artifacts: gems, application 55 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" 56 | COPY --from=build /rails /rails 57 | 58 | # Run and own only the runtime files as a non-root user for security 59 | RUN groupadd --system --gid 1000 rails && \ 60 | useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ 61 | chown -R rails:rails db log storage tmp 62 | USER 1000:1000 63 | 64 | # Entrypoint prepares the database. 65 | ENTRYPOINT ["/rails/bin/docker-entrypoint"] 66 | 67 | # Start server via Thruster by default, this can be overwritten at runtime 68 | EXPOSE 80 69 | CMD ["./bin/thrust", "./bin/rails", "server"] 70 | -------------------------------------------------------------------------------- /apps/rails-api/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 4 | gem "rails", "~> 8.0.0" 5 | # Use sqlite3 as the database for Active Record 6 | gem "sqlite3", ">= 2.1" 7 | # Use the Puma web server [https://github.com/puma/puma] 8 | gem "puma", ">= 5.0" 9 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 10 | # gem "jbuilder" 11 | 12 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 13 | # gem "bcrypt", "~> 3.1.7" 14 | 15 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 16 | gem "tzinfo-data", platforms: %i[windows jruby] 17 | 18 | # Use the database-backed adapters for Rails.cache, Active Job, and Action Cable 19 | gem "solid_cache" 20 | gem "solid_queue" 21 | gem "solid_cable" 22 | 23 | # Reduces boot times through caching; required in config/boot.rb 24 | gem "bootsnap", require: false 25 | 26 | # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] 27 | gem "kamal", require: false 28 | 29 | # Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] 30 | gem "thruster", require: false 31 | 32 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 33 | # gem "image_processing", "~> 1.2" 34 | 35 | # Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin Ajax possible 36 | # gem "rack-cors" 37 | 38 | path "../.." do 39 | gem "clerk-sdk-ruby", require: "clerk" 40 | end 41 | 42 | 43 | group :development, :test do 44 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 45 | gem "debug", platforms: %i[mri windows], require: "debug/prelude" 46 | 47 | # Static analysis for security vulnerabilities [https://brakemanscanner.org/] 48 | gem "brakeman", require: false 49 | 50 | # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] 51 | gem "rubocop-rails-omakase", require: false 52 | end 53 | 54 | gem "dotenv-rails", "~> 3.1" 55 | -------------------------------------------------------------------------------- /apps/rails-api/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | -------------------------------------------------------------------------------- /apps/rails-api/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /apps/rails-api/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include Clerk::Authenticatable 3 | end 4 | -------------------------------------------------------------------------------- /apps/rails-api/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | render json: clerk.user || {message: "Not authenticated"} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /apps/rails-api/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /apps/rails-api/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /apps/rails-api/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /apps/rails-api/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/rails-api/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /apps/rails-api/bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /apps/rails-api/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || 66 | cli_arg_version || 67 | bundler_requirement_for(lockfile_version) 68 | end 69 | 70 | def bundler_requirement_for(version) 71 | return "#{Gem::Requirement.default}.a" unless version 72 | 73 | bundler_gem_version = Gem::Version.new(version) 74 | 75 | bundler_gem_version.approximate_recommendation 76 | end 77 | 78 | def load_bundler! 79 | ENV["BUNDLE_GEMFILE"] ||= gemfile 80 | 81 | activate_bundler 82 | end 83 | 84 | def activate_bundler 85 | gem_error = activation_error_handling do 86 | gem "bundler", bundler_requirement 87 | end 88 | return if gem_error.nil? 89 | require_error = activation_error_handling do 90 | require "bundler/version" 91 | end 92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 93 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | if m.invoked_as_script? 108 | load Gem.bin_path("bundler", "bundle") 109 | end 110 | -------------------------------------------------------------------------------- /apps/rails-api/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /apps/rails-api/bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | exec "${@}" 15 | -------------------------------------------------------------------------------- /apps/rails-api/bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /apps/rails-api/bin/kamal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kamal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("kamal", "kamal") 28 | -------------------------------------------------------------------------------- /apps/rails-api/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /apps/rails-api/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /apps/rails-api/bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /apps/rails-api/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/rails-api/bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /apps/rails-api/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /apps/rails-api/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | # Load environment variables manually from shared .env file 10 | require "dotenv/rails" 11 | Dotenv::Rails.files.unshift(File.expand_path(File.join("..", "..", ".env"))) 12 | 13 | module RailsApi 14 | class Application < Rails::Application 15 | # Initialize configuration defaults for originally generated Rails version. 16 | config.load_defaults 8.0 17 | 18 | # Please, add to the `ignore` list any other `lib` subdirectories that do 19 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 20 | # Common ones are `templates`, `generators`, or `middleware`, for example. 21 | config.autoload_lib(ignore: %w[assets tasks]) 22 | 23 | # Configuration for the application, engines, and railties goes here. 24 | # 25 | # These settings can be overridden in specific environments using the files 26 | # in config/environments, which are processed later. 27 | # 28 | # config.time_zone = "Central Time (US & Canada)" 29 | # config.eager_load_paths << Rails.root.join("extras") 30 | 31 | # Only loads a smaller set of middleware suitable for API only apps. 32 | # Middleware like session, flash, cookies can be added back manually. 33 | # Skip views, helpers and assets when generating a new resource. 34 | config.api_only = true 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /apps/rails-api/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /apps/rails-api/config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: async 7 | 8 | test: 9 | adapter: test 10 | 11 | production: 12 | adapter: solid_cable 13 | connects_to: 14 | database: 15 | writing: cable 16 | polling_interval: 0.1.seconds 17 | message_retention: 1.day 18 | -------------------------------------------------------------------------------- /apps/rails-api/config/cache.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | store_options: 3 | # Cap age of oldest cache entry to fulfill retention policies 4 | # max_age: <%= 60.days.to_i %> 5 | max_size: <%= 256.megabytes %> 6 | namespace: <%= Rails.env %> 7 | 8 | development: 9 | <<: *default 10 | 11 | test: 12 | <<: *default 13 | 14 | production: 15 | database: cache 16 | <<: *default 17 | -------------------------------------------------------------------------------- /apps/rails-api/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | DxqE9vmSeT9Klo+KJDhexo4vQZXc4+vELnjH3Ajkh+aTfnR9mkTMkXhq7FwifMXvmXMtMoc1pZH9GuIJ9hvI3maLlPjYeb7Ktext15q8+xTP0hJrw7vpVQvqxMMC/pRVv0mFbzGBdQaOVer9toCFYSs3L+rrA3mbPOhc9uub+jwWAz3N61gjPOifX4Lu3vqU484m5lK35xHNyACcy2IUxv+1ijk+42pgedWFCiYvuANi3Lc9sb1SHRA8Tn4jVoj842WAQ4abiVg2P4IFGhSaveGP6zmanyoRg11UBlc62SmmnQL9kpaTnDWZqsf16dJ6pNkT3mvgjFRjhy9lvT+gbXLtJNqS8GP13KpO4kmLVD7YNXtjFyyqHamPaUeSUOZ8C8STeSl6YiU+40Ia9yIQ47zsNLLIaRR4MWhQGIEf+H0ZdwmmMOzGUGs+p6R6/RJb6W+JvI4wl8lgLlQBXOhA8Mi8sCYG+1nJQPXRAGIZ3uvH0cQqUGhWodrr--OJs3x4FP6O6jMsR0--oF4iFB4bTnDh0TJVPfIl3w== -------------------------------------------------------------------------------- /apps/rails-api/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: storage/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: storage/test.sqlite3 22 | 23 | 24 | # Store production database in the storage/ directory, which by default 25 | # is mounted as a persistent Docker volume in config/deploy.yml. 26 | production: 27 | primary: 28 | <<: *default 29 | database: storage/production.sqlite3 30 | cache: 31 | <<: *default 32 | database: storage/production_cache.sqlite3 33 | migrations_paths: db/cache_migrate 34 | queue: 35 | <<: *default 36 | database: storage/production_queue.sqlite3 37 | migrations_paths: db/queue_migrate 38 | cable: 39 | <<: *default 40 | database: storage/production_cable.sqlite3 41 | migrations_paths: db/cable_migrate 42 | -------------------------------------------------------------------------------- /apps/rails-api/config/deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of your application. Used to uniquely configure containers. 2 | service: rails_api 3 | 4 | # Name of the container image. 5 | image: your-user/rails_api 6 | 7 | # Deploy to these servers. 8 | servers: 9 | web: 10 | - 192.168.0.1 11 | # job: 12 | # hosts: 13 | # - 192.168.0.1 14 | # cmd: bin/jobs 15 | 16 | # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. 17 | # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. 18 | # 19 | # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. 20 | proxy: 21 | ssl: true 22 | host: app.example.com 23 | 24 | # Credentials for your image host. 25 | registry: 26 | # Specify the registry server, if you're not using Docker Hub 27 | # server: registry.digitalocean.com / ghcr.io / ... 28 | username: your-user 29 | 30 | # Always use an access token rather than real password when possible. 31 | password: 32 | - KAMAL_REGISTRY_PASSWORD 33 | 34 | # Inject ENV variables into containers (secrets come from .kamal/secrets). 35 | env: 36 | secret: 37 | - RAILS_MASTER_KEY 38 | clear: 39 | # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. 40 | # When you start using multiple servers, you should split out job processing to a dedicated machine. 41 | SOLID_QUEUE_IN_PUMA: true 42 | 43 | # Set number of processes dedicated to Solid Queue (default: 1) 44 | # JOB_CONCURRENCY: 3 45 | 46 | # Set number of cores available to the application on each server (default: 1). 47 | # WEB_CONCURRENCY: 2 48 | 49 | # Match this to any external database server to configure Active Record correctly 50 | # Use rails_api-db for a db accessory server on same machine via local kamal docker network. 51 | # DB_HOST: 192.168.0.2 52 | 53 | # Log everything from Rails 54 | # RAILS_LOG_LEVEL: debug 55 | 56 | # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: 57 | # "bin/kamal logs -r job" will tail logs from the first server in the job section. 58 | aliases: 59 | console: app exec --interactive --reuse "bin/rails console" 60 | shell: app exec --interactive --reuse "bash" 61 | logs: app logs -f 62 | dbc: app exec --interactive --reuse "bin/rails dbconsole" 63 | 64 | 65 | # Use a persistent storage volume for sqlite database files and local Active Storage files. 66 | # Recommended to change this to a mounted volume path that is backed up off server. 67 | volumes: 68 | - "rails_api_storage:/rails/storage" 69 | 70 | 71 | # Bridge fingerprinted assets, like JS and CSS, between versions to avoid 72 | # hitting 404 on in-flight requests. Combines all files from new and old 73 | # version inside the asset_path. 74 | asset_path: /rails/public/assets 75 | 76 | # Configure the image builder. 77 | builder: 78 | arch: amd64 79 | 80 | # # Build image via remote server (useful for faster amd64 builds on arm64 computers) 81 | # remote: ssh://docker@docker-builder-server 82 | # 83 | # # Pass arguments and secrets to the Docker build process 84 | # args: 85 | # RUBY_VERSION: 3.3.5 86 | # secrets: 87 | # - GITHUB_TOKEN 88 | # - RAILS_MASTER_KEY 89 | 90 | # Use a different ssh user than root 91 | # ssh: 92 | # user: app 93 | 94 | # Use accessory services (secrets come from .kamal/secrets). 95 | # accessories: 96 | # db: 97 | # image: mysql:8.0 98 | # host: 192.168.0.2 99 | # # Change to 3306 to expose port to the world instead of just local network. 100 | # port: "127.0.0.1:3306:3306" 101 | # env: 102 | # clear: 103 | # MYSQL_ROOT_HOST: '%' 104 | # secret: 105 | # - MYSQL_ROOT_PASSWORD 106 | # files: 107 | # - config/mysql/production.cnf:/etc/mysql/my.cnf 108 | # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql 109 | # directories: 110 | # - data:/var/lib/mysql 111 | # redis: 112 | # image: redis:7.0 113 | # host: 192.168.0.2 114 | # port: 6379 115 | # directories: 116 | # - data:/data 117 | -------------------------------------------------------------------------------- /apps/rails-api/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /apps/rails-api/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } 22 | else 23 | config.action_controller.perform_caching = false 24 | end 25 | 26 | # Change to :null_store to avoid any caching. 27 | config.cache_store = :memory_store 28 | 29 | # Store uploaded files on the local file system (see config/storage.yml for options). 30 | config.active_storage.service = :local 31 | 32 | # Don't care if the mailer can't send. 33 | config.action_mailer.raise_delivery_errors = false 34 | 35 | # Make template changes take effect immediately. 36 | config.action_mailer.perform_caching = false 37 | 38 | # Set localhost to be used by links generated in mailer templates. 39 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 40 | 41 | # Print deprecation notices to the Rails logger. 42 | config.active_support.deprecation = :log 43 | 44 | # Raise an error on page load if there are pending migrations. 45 | config.active_record.migration_error = :page_load 46 | 47 | # Highlight code that triggered database queries in logs. 48 | config.active_record.verbose_query_logs = true 49 | 50 | # Append comments with runtime information tags to SQL queries in logs. 51 | config.active_record.query_log_tags_enabled = true 52 | 53 | # Highlight code that enqueued background job in logs. 54 | config.active_job.verbose_enqueue_logs = true 55 | 56 | # Raises error for missing translations. 57 | # config.i18n.raise_on_missing_translations = true 58 | 59 | # Annotate rendered view with file names. 60 | config.action_view.annotate_rendered_view_with_filenames = true 61 | 62 | # Uncomment if you wish to allow Action Cable access from any origin. 63 | # config.action_cable.disable_request_forgery_protection = true 64 | 65 | # Raise error when a before_action's only/except options reference missing actions. 66 | config.action_controller.raise_on_missing_callback_actions = true 67 | 68 | # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. 69 | # config.generators.apply_rubocop_autocorrect_after_generate! 70 | end 71 | -------------------------------------------------------------------------------- /apps/rails-api/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Cache assets for far-future expiry since they are all digest stamped. 16 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } 17 | 18 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 19 | # config.asset_host = "http://assets.example.com" 20 | 21 | # Store uploaded files on the local file system (see config/storage.yml for options). 22 | config.active_storage.service = :local 23 | 24 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 25 | config.assume_ssl = true 26 | 27 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 28 | config.force_ssl = true 29 | 30 | # Skip http-to-https redirect for the default health check endpoint. 31 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 32 | 33 | # Log to STDOUT with the current request id as a default log tag. 34 | config.log_tags = [ :request_id ] 35 | config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) 36 | 37 | # Change to "debug" to log everything (including potentially personally-identifiable information!) 38 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 39 | 40 | # Prevent health checks from clogging up the logs. 41 | config.silence_healthcheck_path = "/up" 42 | 43 | # Don't log any deprecations. 44 | config.active_support.report_deprecations = false 45 | 46 | # Replace the default in-process memory cache store with a durable alternative. 47 | config.cache_store = :solid_cache_store 48 | 49 | # Replace the default in-process and non-durable queuing backend for Active Job. 50 | config.active_job.queue_adapter = :solid_queue 51 | config.solid_queue.connects_to = { database: { writing: :queue } } 52 | 53 | 54 | # Ignore bad email addresses and do not raise email delivery errors. 55 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 56 | # config.action_mailer.raise_delivery_errors = false 57 | 58 | # Set host to be used by links generated in mailer templates. 59 | config.action_mailer.default_url_options = { host: "example.com" } 60 | 61 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. 62 | # config.action_mailer.smtp_settings = { 63 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 64 | # password: Rails.application.credentials.dig(:smtp, :password), 65 | # address: "smtp.example.com", 66 | # port: 587, 67 | # authentication: :plain 68 | # } 69 | 70 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 71 | # the I18n.default_locale when a translation cannot be found). 72 | config.i18n.fallbacks = true 73 | 74 | # Do not dump schema after migrations. 75 | config.active_record.dump_schema_after_migration = false 76 | 77 | # Only use :id for inspections in production. 78 | config.active_record.attributes_for_inspect = [ :id ] 79 | 80 | # Enable DNS rebinding protection and other `Host` header attacks. 81 | # config.hosts = [ 82 | # "example.com", # Allow requests from example.com 83 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 84 | # ] 85 | # 86 | # Skip DNS rebinding protection for the default health check endpoint. 87 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 88 | end 89 | -------------------------------------------------------------------------------- /apps/rails-api/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: "example.com" } 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | end 54 | -------------------------------------------------------------------------------- /apps/rails-api/config/initializers/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin Ajax requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins "example.com" 11 | # 12 | # resource "*", 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /apps/rails-api/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /apps/rails-api/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /apps/rails-api/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | -------------------------------------------------------------------------------- /apps/rails-api/config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 37 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 38 | 39 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 40 | # In other environments, only set the PID file if requested. 41 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 42 | -------------------------------------------------------------------------------- /apps/rails-api/config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /apps/rails-api/config/recurring.yml: -------------------------------------------------------------------------------- 1 | # production: 2 | # periodic_cleanup: 3 | # class: CleanSoftDeletedRecordsJob 4 | # queue: background 5 | # args: [ 1000, { batch_size: 500 } ] 6 | # schedule: every hour 7 | # periodic_command: 8 | # command: "SoftDeletedRecord.due.delete_all" 9 | # priority: 2 10 | # schedule: at 5am every day 11 | -------------------------------------------------------------------------------- /apps/rails-api/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | 4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 5 | # Can be used by load balancers and uptime monitors to verify that the app is live. 6 | get "up" => "rails/health#show", :as => :rails_health_check 7 | 8 | # Defines the root path route ("/") 9 | root "home#index" 10 | end 11 | -------------------------------------------------------------------------------- /apps/rails-api/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /apps/rails-api/db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_cable_messages", force: :cascade do |t| 3 | t.binary "channel", limit: 1024, null: false 4 | t.binary "payload", limit: 536870912, null: false 5 | t.datetime "created_at", null: false 6 | t.integer "channel_hash", limit: 8, null: false 7 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 8 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 9 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /apps/rails-api/db/cache_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema[7.2].define(version: 1) do 4 | create_table "solid_cache_entries", force: :cascade do |t| 5 | t.binary "key", limit: 1024, null: false 6 | t.binary "value", limit: 536870912, null: false 7 | t.datetime "created_at", null: false 8 | t.integer "key_hash", limit: 8, null: false 9 | t.integer "byte_size", limit: 4, null: false 10 | t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" 11 | t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" 12 | t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/rails-api/db/queue_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_queue_blocked_executions", force: :cascade do |t| 3 | t.bigint "job_id", null: false 4 | t.string "queue_name", null: false 5 | t.integer "priority", default: 0, null: false 6 | t.string "concurrency_key", null: false 7 | t.datetime "expires_at", null: false 8 | t.datetime "created_at", null: false 9 | t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" 10 | t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" 11 | t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true 12 | end 13 | 14 | create_table "solid_queue_claimed_executions", force: :cascade do |t| 15 | t.bigint "job_id", null: false 16 | t.bigint "process_id" 17 | t.datetime "created_at", null: false 18 | t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true 19 | t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" 20 | end 21 | 22 | create_table "solid_queue_failed_executions", force: :cascade do |t| 23 | t.bigint "job_id", null: false 24 | t.text "error" 25 | t.datetime "created_at", null: false 26 | t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true 27 | end 28 | 29 | create_table "solid_queue_jobs", force: :cascade do |t| 30 | t.string "queue_name", null: false 31 | t.string "class_name", null: false 32 | t.text "arguments" 33 | t.integer "priority", default: 0, null: false 34 | t.string "active_job_id" 35 | t.datetime "scheduled_at" 36 | t.datetime "finished_at" 37 | t.string "concurrency_key" 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" 41 | t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" 42 | t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" 43 | t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" 44 | t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" 45 | end 46 | 47 | create_table "solid_queue_pauses", force: :cascade do |t| 48 | t.string "queue_name", null: false 49 | t.datetime "created_at", null: false 50 | t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true 51 | end 52 | 53 | create_table "solid_queue_processes", force: :cascade do |t| 54 | t.string "kind", null: false 55 | t.datetime "last_heartbeat_at", null: false 56 | t.bigint "supervisor_id" 57 | t.integer "pid", null: false 58 | t.string "hostname" 59 | t.text "metadata" 60 | t.datetime "created_at", null: false 61 | t.string "name", null: false 62 | t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" 63 | t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true 64 | t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" 65 | end 66 | 67 | create_table "solid_queue_ready_executions", force: :cascade do |t| 68 | t.bigint "job_id", null: false 69 | t.string "queue_name", null: false 70 | t.integer "priority", default: 0, null: false 71 | t.datetime "created_at", null: false 72 | t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true 73 | t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" 74 | t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" 75 | end 76 | 77 | create_table "solid_queue_recurring_executions", force: :cascade do |t| 78 | t.bigint "job_id", null: false 79 | t.string "task_key", null: false 80 | t.datetime "run_at", null: false 81 | t.datetime "created_at", null: false 82 | t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true 83 | t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true 84 | end 85 | 86 | create_table "solid_queue_recurring_tasks", force: :cascade do |t| 87 | t.string "key", null: false 88 | t.string "schedule", null: false 89 | t.string "command", limit: 2048 90 | t.string "class_name" 91 | t.text "arguments" 92 | t.string "queue_name" 93 | t.integer "priority", default: 0 94 | t.boolean "static", default: true, null: false 95 | t.text "description" 96 | t.datetime "created_at", null: false 97 | t.datetime "updated_at", null: false 98 | t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true 99 | t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" 100 | end 101 | 102 | create_table "solid_queue_scheduled_executions", force: :cascade do |t| 103 | t.bigint "job_id", null: false 104 | t.string "queue_name", null: false 105 | t.integer "priority", default: 0, null: false 106 | t.datetime "scheduled_at", null: false 107 | t.datetime "created_at", null: false 108 | t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true 109 | t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" 110 | end 111 | 112 | create_table "solid_queue_semaphores", force: :cascade do |t| 113 | t.string "key", null: false 114 | t.integer "value", default: 1, null: false 115 | t.datetime "expires_at", null: false 116 | t.datetime "created_at", null: false 117 | t.datetime "updated_at", null: false 118 | t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" 119 | t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" 120 | t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true 121 | end 122 | 123 | add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 124 | add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 125 | add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 126 | add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 127 | add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 128 | add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 129 | end 130 | -------------------------------------------------------------------------------- /apps/rails-api/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should ensure the existence of records required to run the application in every environment (production, 2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Example: 6 | # 7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| 8 | # MovieGenre.find_or_create_by!(name: genre_name) 9 | # end 10 | -------------------------------------------------------------------------------- /apps/rails-api/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /apps/rails-api/test/controllers/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HomeControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /apps/rails-api/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | module ActiveSupport 6 | class TestCase 7 | # Run tests in parallel with specified workers 8 | parallelize(workers: :number_of_processors) 9 | 10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /apps/rails-full/.dockerignore: -------------------------------------------------------------------------------- 1 | # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. 2 | 3 | # Ignore git directory. 4 | /.git/ 5 | /.gitignore 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all default key files. 14 | /config/master.key 15 | /config/credentials/*.key 16 | 17 | # Ignore all logfiles and tempfiles. 18 | /log/* 19 | /tmp/* 20 | !/log/.keep 21 | !/tmp/.keep 22 | 23 | # Ignore pidfiles, but keep the directory. 24 | /tmp/pids/* 25 | !/tmp/pids/.keep 26 | 27 | # Ignore storage (uploaded files in development and any SQLite databases). 28 | /storage/* 29 | !/storage/.keep 30 | /tmp/storage/* 31 | !/tmp/storage/.keep 32 | 33 | # Ignore assets. 34 | /node_modules/ 35 | /app/assets/builds/* 36 | !/app/assets/builds/.keep 37 | /public/assets 38 | 39 | # Ignore CI service files. 40 | /.github 41 | 42 | # Ignore development files 43 | /.devcontainer 44 | 45 | # Ignore Docker-related files 46 | /.dockerignore 47 | /Dockerfile* 48 | -------------------------------------------------------------------------------- /apps/rails-full/.gitattributes: -------------------------------------------------------------------------------- 1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files. 2 | 3 | # Mark the database schema as having been generated. 4 | db/schema.rb linguist-generated 5 | 6 | # Mark any vendored files as having been vendored. 7 | vendor/* linguist-vendored 8 | config/credentials/*.yml.enc diff=rails_credentials 9 | config/credentials.yml.enc diff=rails_credentials 10 | -------------------------------------------------------------------------------- /apps/rails-full/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # Temporary files generated by your text editor or operating system 4 | # belong in git's global ignore instead: 5 | # `$XDG_CONFIG_HOME/git/ignore` or `~/.config/git/ignore` 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore all environment files. 11 | /.env* 12 | 13 | # Ignore all logfiles and tempfiles. 14 | /log/* 15 | /tmp/* 16 | !/log/.keep 17 | !/tmp/.keep 18 | 19 | # Ignore pidfiles, but keep the directory. 20 | /tmp/pids/* 21 | !/tmp/pids/ 22 | !/tmp/pids/.keep 23 | 24 | # Ignore storage (uploaded files in development and any SQLite databases). 25 | /storage/* 26 | !/storage/.keep 27 | /tmp/storage/* 28 | !/tmp/storage/ 29 | !/tmp/storage/.keep 30 | 31 | /public/assets 32 | 33 | # Ignore master key for decrypting credentials and more. 34 | /config/master.key 35 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/docker-setup.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Docker set up on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/post-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample post-deploy hook 4 | # 5 | # These environment variables are available: 6 | # KAMAL_RECORDED_AT 7 | # KAMAL_PERFORMER 8 | # KAMAL_VERSION 9 | # KAMAL_HOSTS 10 | # KAMAL_ROLE (if set) 11 | # KAMAL_DESTINATION (if set) 12 | # KAMAL_RUNTIME 13 | 14 | echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds" 15 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/post-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooted kamal-proxy on $KAMAL_HOSTS" 4 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/pre-build.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # A sample pre-build hook 4 | # 5 | # Checks: 6 | # 1. We have a clean checkout 7 | # 2. A remote is configured 8 | # 3. The branch has been pushed to the remote 9 | # 4. The version we are deploying matches the remote 10 | # 11 | # These environment variables are available: 12 | # KAMAL_RECORDED_AT 13 | # KAMAL_PERFORMER 14 | # KAMAL_VERSION 15 | # KAMAL_HOSTS 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | if [ -n "$(git status --porcelain)" ]; then 20 | echo "Git checkout is not clean, aborting..." >&2 21 | git status --porcelain >&2 22 | exit 1 23 | fi 24 | 25 | first_remote=$(git remote) 26 | 27 | if [ -z "$first_remote" ]; then 28 | echo "No git remote set, aborting..." >&2 29 | exit 1 30 | fi 31 | 32 | current_branch=$(git branch --show-current) 33 | 34 | if [ -z "$current_branch" ]; then 35 | echo "Not on a git branch, aborting..." >&2 36 | exit 1 37 | fi 38 | 39 | remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1) 40 | 41 | if [ -z "$remote_head" ]; then 42 | echo "Branch not pushed to remote, aborting..." >&2 43 | exit 1 44 | fi 45 | 46 | if [ "$KAMAL_VERSION" != "$remote_head" ]; then 47 | echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2 48 | exit 1 49 | fi 50 | 51 | exit 0 52 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/pre-connect.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-connect check 4 | # 5 | # Warms DNS before connecting to hosts in parallel 6 | # 7 | # These environment variables are available: 8 | # KAMAL_RECORDED_AT 9 | # KAMAL_PERFORMER 10 | # KAMAL_VERSION 11 | # KAMAL_HOSTS 12 | # KAMAL_ROLE (if set) 13 | # KAMAL_DESTINATION (if set) 14 | # KAMAL_RUNTIME 15 | 16 | hosts = ENV["KAMAL_HOSTS"].split(",") 17 | results = nil 18 | max = 3 19 | 20 | elapsed = Benchmark.realtime do 21 | results = hosts.map do |host| 22 | Thread.new do 23 | tries = 1 24 | 25 | begin 26 | Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME) 27 | rescue SocketError 28 | if tries < max 29 | puts "Retrying DNS warmup: #{host}" 30 | tries += 1 31 | sleep rand 32 | retry 33 | else 34 | puts "DNS warmup failed: #{host}" 35 | host 36 | end 37 | end 38 | 39 | tries 40 | end 41 | end.map(&:value) 42 | end 43 | 44 | retries = results.sum - hosts.size 45 | nopes = results.count { |r| r == max } 46 | 47 | puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ] 48 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/pre-deploy.sample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # A sample pre-deploy hook 4 | # 5 | # Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds. 6 | # 7 | # Fails unless the combined status is "success" 8 | # 9 | # These environment variables are available: 10 | # KAMAL_RECORDED_AT 11 | # KAMAL_PERFORMER 12 | # KAMAL_VERSION 13 | # KAMAL_HOSTS 14 | # KAMAL_COMMAND 15 | # KAMAL_SUBCOMMAND 16 | # KAMAL_ROLE (if set) 17 | # KAMAL_DESTINATION (if set) 18 | 19 | # Only check the build status for production deployments 20 | if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production" 21 | exit 0 22 | end 23 | 24 | require "bundler/inline" 25 | 26 | # true = install gems so this is fast on repeat invocations 27 | gemfile(true, quiet: true) do 28 | source "https://rubygems.org" 29 | 30 | gem "octokit" 31 | gem "faraday-retry" 32 | end 33 | 34 | MAX_ATTEMPTS = 72 35 | ATTEMPTS_GAP = 10 36 | 37 | def exit_with_error(message) 38 | $stderr.puts message 39 | exit 1 40 | end 41 | 42 | class GithubStatusChecks 43 | attr_reader :remote_url, :git_sha, :github_client, :combined_status 44 | 45 | def initialize 46 | @remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/") 47 | @git_sha = `git rev-parse HEAD`.strip 48 | @github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"]) 49 | refresh! 50 | end 51 | 52 | def refresh! 53 | @combined_status = github_client.combined_status(remote_url, git_sha) 54 | end 55 | 56 | def state 57 | combined_status[:state] 58 | end 59 | 60 | def first_status_url 61 | first_status = combined_status[:statuses].find { |status| status[:state] == state } 62 | first_status && first_status[:target_url] 63 | end 64 | 65 | def complete_count 66 | combined_status[:statuses].count { |status| status[:state] != "pending"} 67 | end 68 | 69 | def total_count 70 | combined_status[:statuses].count 71 | end 72 | 73 | def current_status 74 | if total_count > 0 75 | "Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..." 76 | else 77 | "Build not started..." 78 | end 79 | end 80 | end 81 | 82 | 83 | $stdout.sync = true 84 | 85 | puts "Checking build status..." 86 | attempts = 0 87 | checks = GithubStatusChecks.new 88 | 89 | begin 90 | loop do 91 | case checks.state 92 | when "success" 93 | puts "Checks passed, see #{checks.first_status_url}" 94 | exit 0 95 | when "failure" 96 | exit_with_error "Checks failed, see #{checks.first_status_url}" 97 | when "pending" 98 | attempts += 1 99 | end 100 | 101 | exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS 102 | 103 | puts checks.current_status 104 | sleep(ATTEMPTS_GAP) 105 | checks.refresh! 106 | end 107 | rescue Octokit::NotFound 108 | exit_with_error "Build status could not be found" 109 | end 110 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/hooks/pre-proxy-reboot.sample: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Rebooting kamal-proxy on $KAMAL_HOSTS..." 4 | -------------------------------------------------------------------------------- /apps/rails-full/.kamal/secrets: -------------------------------------------------------------------------------- 1 | # Secrets defined here are available for reference under registry/password, env/secret, builder/secrets, 2 | # and accessories/*/env/secret in config/deploy.yml. All secrets should be pulled from either 3 | # password manager, ENV, or a file. DO NOT ENTER RAW CREDENTIALS HERE! This file needs to be safe for git. 4 | 5 | # Example of extracting secrets from 1password (or another compatible pw manager) 6 | # SECRETS=$(kamal secrets fetch --adapter 1password --account your-account --from Vault/Item KAMAL_REGISTRY_PASSWORD RAILS_MASTER_KEY) 7 | # KAMAL_REGISTRY_PASSWORD=$(kamal secrets extract KAMAL_REGISTRY_PASSWORD ${SECRETS}) 8 | # RAILS_MASTER_KEY=$(kamal secrets extract RAILS_MASTER_KEY ${SECRETS}) 9 | 10 | # Use a GITHUB_TOKEN if private repositories are needed for the image 11 | # GITHUB_TOKEN=$(gh config get -h github.com oauth_token) 12 | 13 | # Grab the registry password from ENV 14 | KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD 15 | 16 | # Improve security by using a password manager. Never check config/master.key into git! 17 | RAILS_MASTER_KEY=$(cat config/master.key) 18 | -------------------------------------------------------------------------------- /apps/rails-full/.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Omakase Ruby styling for Rails 2 | inherit_gem: { rubocop-rails-omakase: rubocop.yml } 3 | 4 | # Overwrite or add rules to create your own house style 5 | # 6 | # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]` 7 | # Layout/SpaceInsideArrayLiteralBrackets: 8 | # Enabled: false 9 | -------------------------------------------------------------------------------- /apps/rails-full/.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.5 2 | -------------------------------------------------------------------------------- /apps/rails-full/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | # check=error=true 3 | 4 | # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: 5 | # docker build -t rails_full . 6 | # docker run -d -p 80:80 -e RAILS_MASTER_KEY= --name rails_full rails_full 7 | 8 | # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html 9 | 10 | # Make sure RUBY_VERSION matches the Ruby version in .ruby-version 11 | ARG RUBY_VERSION=3.3.5 12 | FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base 13 | 14 | # Rails app lives here 15 | WORKDIR /rails 16 | 17 | # Install base packages 18 | RUN apt-get update -qq && \ 19 | apt-get install --no-install-recommends -y curl libjemalloc2 libvips sqlite3 && \ 20 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 21 | 22 | # Set production environment 23 | ENV RAILS_ENV="production" \ 24 | BUNDLE_DEPLOYMENT="1" \ 25 | BUNDLE_PATH="/usr/local/bundle" \ 26 | BUNDLE_WITHOUT="development" 27 | 28 | # Throw-away build stage to reduce size of final image 29 | FROM base AS build 30 | 31 | # Install packages needed to build gems 32 | RUN apt-get update -qq && \ 33 | apt-get install --no-install-recommends -y build-essential git pkg-config && \ 34 | rm -rf /var/lib/apt/lists /var/cache/apt/archives 35 | 36 | # Install application gems 37 | COPY Gemfile Gemfile.lock ./ 38 | RUN bundle install && \ 39 | rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ 40 | bundle exec bootsnap precompile --gemfile 41 | 42 | # Copy application code 43 | COPY . . 44 | 45 | # Precompile bootsnap code for faster boot times 46 | RUN bundle exec bootsnap precompile app/ lib/ 47 | 48 | # Precompiling assets for production without requiring secret RAILS_MASTER_KEY 49 | RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile 50 | 51 | 52 | 53 | 54 | # Final stage for app image 55 | FROM base 56 | 57 | # Copy built artifacts: gems, application 58 | COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" 59 | COPY --from=build /rails /rails 60 | 61 | # Run and own only the runtime files as a non-root user for security 62 | RUN groupadd --system --gid 1000 rails && \ 63 | useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ 64 | chown -R rails:rails db log storage tmp 65 | USER 1000:1000 66 | 67 | # Entrypoint prepares the database. 68 | ENTRYPOINT ["/rails/bin/docker-entrypoint"] 69 | 70 | # Start server via Thruster by default, this can be overwritten at runtime 71 | EXPOSE 80 72 | CMD ["./bin/thrust", "./bin/rails", "server"] 73 | -------------------------------------------------------------------------------- /apps/rails-full/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 4 | gem "rails", "~> 8.0.0" 5 | # The modern asset pipeline for Rails [https://github.com/rails/propshaft] 6 | gem "propshaft" 7 | # Use sqlite3 as the database for Active Record 8 | gem "sqlite3", ">= 2.1" 9 | # Use the Puma web server [https://github.com/puma/puma] 10 | gem "puma", ">= 5.0" 11 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 12 | gem "importmap-rails" 13 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 14 | gem "turbo-rails" 15 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 16 | gem "stimulus-rails" 17 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 18 | gem "jbuilder" 19 | 20 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 21 | # gem "bcrypt", "~> 3.1.7" 22 | 23 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 24 | gem "tzinfo-data", platforms: %i[windows jruby] 25 | 26 | # Use the database-backed adapters for Rails.cache, Active Job, and Action Cable 27 | gem "solid_cache" 28 | gem "solid_queue" 29 | gem "solid_cable" 30 | 31 | # Reduces boot times through caching; required in config/boot.rb 32 | gem "bootsnap", require: false 33 | 34 | # Deploy this application anywhere as a Docker container [https://kamal-deploy.org] 35 | gem "kamal", require: false 36 | 37 | # Add HTTP asset caching/compression and X-Sendfile acceleration to Puma [https://github.com/basecamp/thruster/] 38 | gem "thruster", require: false 39 | 40 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 41 | # gem "image_processing", "~> 1.2" 42 | 43 | path "../.." do 44 | gem "clerk-sdk-ruby", require: "clerk" 45 | end 46 | 47 | 48 | group :development, :test do 49 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 50 | gem "debug", platforms: %i[mri windows], require: "debug/prelude" 51 | 52 | # Static analysis for security vulnerabilities [https://brakemanscanner.org/] 53 | gem "brakeman", require: false 54 | 55 | # Omakase Ruby styling [https://github.com/rails/rubocop-rails-omakase/] 56 | gem "rubocop-rails-omakase", require: false 57 | end 58 | 59 | group :development do 60 | # Use console on exceptions pages [https://github.com/rails/web-console] 61 | gem "web-console" 62 | end 63 | 64 | group :test do 65 | # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] 66 | gem "capybara" 67 | gem "selenium-webdriver" 68 | end 69 | 70 | gem "dotenv-rails", "~> 3.1" 71 | -------------------------------------------------------------------------------- /apps/rails-full/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | -------------------------------------------------------------------------------- /apps/rails-full/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /apps/rails-full/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css. 3 | * 4 | * With Propshaft, assets are served efficiently without preprocessing steps. You can still include 5 | * application-wide styles in this file, but keep in mind that CSS precedence will follow the standard 6 | * cascading order, meaning styles declared later in the document or manifest will override earlier ones, 7 | * depending on specificity. 8 | * 9 | * Consider organizing styles into separate files for maintainability. 10 | */ 11 | -------------------------------------------------------------------------------- /apps/rails-full/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Clerk::Authenticatable 3 | 4 | # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. 5 | allow_browser versions: :modern 6 | end 7 | -------------------------------------------------------------------------------- /apps/rails-full/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | before_action :require_reverification!, only: [:protected], preset: Clerk::StepUp::Preset::STRICT 3 | 4 | def index 5 | @user = clerk.user 6 | end 7 | 8 | def protected 9 | render json: {message: clerk.user? ? "Valid session" : "Not logged in"} 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /apps/rails-full/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /apps/rails-full/app/helpers/home_helper.rb: -------------------------------------------------------------------------------- 1 | module HomeHelper 2 | end 3 | -------------------------------------------------------------------------------- /apps/rails-full/app/javascript/application.js: -------------------------------------------------------------------------------- 1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails 2 | import "@hotwired/turbo-rails" 3 | import "controllers" 4 | -------------------------------------------------------------------------------- /apps/rails-full/app/javascript/controllers/application.js: -------------------------------------------------------------------------------- 1 | import { Application } from "@hotwired/stimulus" 2 | 3 | const application = Application.start() 4 | 5 | // Configure Stimulus development experience 6 | application.debug = false 7 | window.Stimulus = application 8 | 9 | export { application } 10 | -------------------------------------------------------------------------------- /apps/rails-full/app/javascript/controllers/hello_controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "@hotwired/stimulus" 2 | 3 | export default class extends Controller { 4 | connect() { 5 | this.element.textContent = "Hello World!" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/rails-full/app/javascript/controllers/index.js: -------------------------------------------------------------------------------- 1 | // Import and register all your controllers from the importmap via controllers/**/*_controller 2 | import { application } from "controllers/application" 3 | import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" 4 | eagerLoadControllersFrom("controllers", application) 5 | -------------------------------------------------------------------------------- /apps/rails-full/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /apps/rails-full/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /apps/rails-full/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /apps/rails-full/app/views/home/index.html.erb: -------------------------------------------------------------------------------- 1 |

2 | <% if @user %> 3 | Authenticated User: <%= @user.first_name %> (<%= @user.id %>) 4 | <% else %> 5 | Not Authenticated 6 | <% end %> 7 |

-------------------------------------------------------------------------------- /apps/rails-full/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= content_for(:title) || "Rails Full" %> 5 | 6 | 7 | 8 | <%= csrf_meta_tags %> 9 | <%= csp_meta_tag %> 10 | 11 | <%= yield :head %> 12 | 13 | <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> 14 | <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> 15 | 16 | 17 | 18 | 19 | 20 | 29 | 36 | 49 | 50 | <%# Includes all stylesheet files in app/assets/stylesheets %> 51 | <%= stylesheet_link_tag :app, "data-turbo-track": "reload" %> 52 | <%= javascript_importmap_tags %> 53 | 54 | 55 | 56 |

Rails (Full Stack)

57 | <%= yield %> 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /apps/rails-full/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/rails-full/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /apps/rails-full/app/views/pwa/manifest.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RailsFull", 3 | "icons": [ 4 | { 5 | "src": "/icon.png", 6 | "type": "image/png", 7 | "sizes": "512x512" 8 | }, 9 | { 10 | "src": "/icon.png", 11 | "type": "image/png", 12 | "sizes": "512x512", 13 | "purpose": "maskable" 14 | } 15 | ], 16 | "start_url": "/", 17 | "display": "standalone", 18 | "scope": "/", 19 | "description": "RailsFull.", 20 | "theme_color": "red", 21 | "background_color": "red" 22 | } 23 | -------------------------------------------------------------------------------- /apps/rails-full/app/views/pwa/service-worker.js: -------------------------------------------------------------------------------- 1 | // Add a service worker for processing Web Push notifications: 2 | // 3 | // self.addEventListener("push", async (event) => { 4 | // const { title, options } = await event.data.json() 5 | // event.waitUntil(self.registration.showNotification(title, options)) 6 | // }) 7 | // 8 | // self.addEventListener("notificationclick", function(event) { 9 | // event.notification.close() 10 | // event.waitUntil( 11 | // clients.matchAll({ type: "window" }).then((clientList) => { 12 | // for (let i = 0; i < clientList.length; i++) { 13 | // let client = clientList[i] 14 | // let clientPath = (new URL(client.url)).pathname 15 | // 16 | // if (clientPath == event.notification.data.path && "focus" in client) { 17 | // return client.focus() 18 | // } 19 | // } 20 | // 21 | // if (clients.openWindow) { 22 | // return clients.openWindow(event.notification.data.path) 23 | // } 24 | // }) 25 | // ) 26 | // }) 27 | -------------------------------------------------------------------------------- /apps/rails-full/bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /apps/rails-full/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'bundle' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | require "rubygems" 12 | 13 | m = Module.new do 14 | module_function 15 | 16 | def invoked_as_script? 17 | File.expand_path($0) == File.expand_path(__FILE__) 18 | end 19 | 20 | def env_var_version 21 | ENV["BUNDLER_VERSION"] 22 | end 23 | 24 | def cli_arg_version 25 | return unless invoked_as_script? # don't want to hijack other binstubs 26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` 27 | bundler_version = nil 28 | update_index = nil 29 | ARGV.each_with_index do |a, i| 30 | if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) 31 | bundler_version = a 32 | end 33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ 34 | bundler_version = $1 35 | update_index = i 36 | end 37 | bundler_version 38 | end 39 | 40 | def gemfile 41 | gemfile = ENV["BUNDLE_GEMFILE"] 42 | return gemfile if gemfile && !gemfile.empty? 43 | 44 | File.expand_path("../Gemfile", __dir__) 45 | end 46 | 47 | def lockfile 48 | lockfile = 49 | case File.basename(gemfile) 50 | when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") 51 | else "#{gemfile}.lock" 52 | end 53 | File.expand_path(lockfile) 54 | end 55 | 56 | def lockfile_version 57 | return unless File.file?(lockfile) 58 | lockfile_contents = File.read(lockfile) 59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ 60 | Regexp.last_match(1) 61 | end 62 | 63 | def bundler_requirement 64 | @bundler_requirement ||= 65 | env_var_version || 66 | cli_arg_version || 67 | bundler_requirement_for(lockfile_version) 68 | end 69 | 70 | def bundler_requirement_for(version) 71 | return "#{Gem::Requirement.default}.a" unless version 72 | 73 | bundler_gem_version = Gem::Version.new(version) 74 | 75 | bundler_gem_version.approximate_recommendation 76 | end 77 | 78 | def load_bundler! 79 | ENV["BUNDLE_GEMFILE"] ||= gemfile 80 | 81 | activate_bundler 82 | end 83 | 84 | def activate_bundler 85 | gem_error = activation_error_handling do 86 | gem "bundler", bundler_requirement 87 | end 88 | return if gem_error.nil? 89 | require_error = activation_error_handling do 90 | require "bundler/version" 91 | end 92 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) 93 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" 94 | exit 42 95 | end 96 | 97 | def activation_error_handling 98 | yield 99 | nil 100 | rescue StandardError, LoadError => e 101 | e 102 | end 103 | end 104 | 105 | m.load_bundler! 106 | 107 | if m.invoked_as_script? 108 | load Gem.bin_path("bundler", "bundle") 109 | end 110 | -------------------------------------------------------------------------------- /apps/rails-full/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /apps/rails-full/bin/docker-entrypoint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Enable jemalloc for reduced memory usage and latency. 4 | if [ -z "${LD_PRELOAD+x}" ]; then 5 | LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit) 6 | export LD_PRELOAD 7 | fi 8 | 9 | # If running the rails server then create or migrate existing database 10 | if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then 11 | ./bin/rails db:prepare 12 | fi 13 | 14 | exec "${@}" 15 | -------------------------------------------------------------------------------- /apps/rails-full/bin/importmap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/application" 4 | require "importmap/commands" 5 | -------------------------------------------------------------------------------- /apps/rails-full/bin/jobs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require_relative "../config/environment" 4 | require "solid_queue/cli" 5 | 6 | SolidQueue::Cli.start(ARGV) 7 | -------------------------------------------------------------------------------- /apps/rails-full/bin/kamal: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'kamal' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("kamal", "kamal") 28 | -------------------------------------------------------------------------------- /apps/rails-full/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /apps/rails-full/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /apps/rails-full/bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /apps/rails-full/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | STDOUT.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /apps/rails-full/bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /apps/rails-full/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /apps/rails-full/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | # Load environment variables manually from shared .env file 10 | require "dotenv/rails" 11 | Dotenv::Rails.files.unshift(File.expand_path(File.join("..", "..", ".env"))) 12 | 13 | module RailsFull 14 | class Application < Rails::Application 15 | # Initialize configuration defaults for originally generated Rails version. 16 | config.load_defaults 8.0 17 | 18 | # Please, add to the `ignore` list any other `lib` subdirectories that do 19 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 20 | # Common ones are `templates`, `generators`, or `middleware`, for example. 21 | config.autoload_lib(ignore: %w[assets tasks]) 22 | 23 | # Configuration for the application, engines, and railties goes here. 24 | # 25 | # These settings can be overridden in specific environments using the files 26 | # in config/environments, which are processed later. 27 | # 28 | # config.time_zone = "Central Time (US & Canada)" 29 | # config.eager_load_paths << Rails.root.join("extras") 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /apps/rails-full/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /apps/rails-full/config/cable.yml: -------------------------------------------------------------------------------- 1 | # Async adapter only works within the same process, so for manually triggering cable updates from a console, 2 | # and seeing results in the browser, you must do so from the web console (running inside the dev process), 3 | # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view 4 | # to make the web console appear. 5 | development: 6 | adapter: async 7 | 8 | test: 9 | adapter: test 10 | 11 | production: 12 | adapter: solid_cable 13 | connects_to: 14 | database: 15 | writing: cable 16 | polling_interval: 0.1.seconds 17 | message_retention: 1.day 18 | -------------------------------------------------------------------------------- /apps/rails-full/config/cache.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | store_options: 3 | # Cap age of oldest cache entry to fulfill retention policies 4 | # max_age: <%= 60.days.to_i %> 5 | max_size: <%= 256.megabytes %> 6 | namespace: <%= Rails.env %> 7 | 8 | development: 9 | <<: *default 10 | 11 | test: 12 | <<: *default 13 | 14 | production: 15 | database: cache 16 | <<: *default 17 | -------------------------------------------------------------------------------- /apps/rails-full/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | HjFmR7rvjb6I8x4lUwbNJW7XxLVXkqUz2gONRhdXNK4flRFut+273GglF3i0hxNc/au4TlXai1EdH4dnE3nlnT/FADvU07kG8J5Q/HTT6IlvObtcw4uDgFtMVx/IvbitIjB9c2amaFKbBucjy9nBpopewh4TZbmJevA1QtGRI9V3A6eDeUuTHM3MM34q7oKV/kx5EH3njPxYGx6lWwQMmTDhkLB0bjf6erdNRVsglZVXmSs9opTO5vmoAkOBFb4GdtOXq297BBNTW2Cnc/BhW403IuIMxBX9R9299n0TNsRSITWKnXZ+VqPlxwM2/tZwiZlfd5R22KqzZC4a7dRdwZ24y46LBMsIk+6q2rosV0a6S+hN+FDYTkGMjj2kYlYjfMKJjIGf6xM8n59YOgh/Ir9V/ouf42W30ZDFkauX8XL0ygQRvICESGzg4QEakkoPJoHboiUmnUoOBCfUbXg50C2+dAiFUayKO8vORg4UsVXSdg6AES9K9rk6--5BPhfQGoIFXv/74y--dDR6BykRl0GAw/Dc89/vQQ== -------------------------------------------------------------------------------- /apps/rails-full/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: storage/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: storage/test.sqlite3 22 | 23 | 24 | # Store production database in the storage/ directory, which by default 25 | # is mounted as a persistent Docker volume in config/deploy.yml. 26 | production: 27 | primary: 28 | <<: *default 29 | database: storage/production.sqlite3 30 | cache: 31 | <<: *default 32 | database: storage/production_cache.sqlite3 33 | migrations_paths: db/cache_migrate 34 | queue: 35 | <<: *default 36 | database: storage/production_queue.sqlite3 37 | migrations_paths: db/queue_migrate 38 | cable: 39 | <<: *default 40 | database: storage/production_cable.sqlite3 41 | migrations_paths: db/cable_migrate 42 | -------------------------------------------------------------------------------- /apps/rails-full/config/deploy.yml: -------------------------------------------------------------------------------- 1 | # Name of your application. Used to uniquely configure containers. 2 | service: rails_full 3 | 4 | # Name of the container image. 5 | image: your-user/rails_full 6 | 7 | # Deploy to these servers. 8 | servers: 9 | web: 10 | - 192.168.0.1 11 | # job: 12 | # hosts: 13 | # - 192.168.0.1 14 | # cmd: bin/jobs 15 | 16 | # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. 17 | # Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer. 18 | # 19 | # Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. 20 | proxy: 21 | ssl: true 22 | host: app.example.com 23 | 24 | # Credentials for your image host. 25 | registry: 26 | # Specify the registry server, if you're not using Docker Hub 27 | # server: registry.digitalocean.com / ghcr.io / ... 28 | username: your-user 29 | 30 | # Always use an access token rather than real password when possible. 31 | password: 32 | - KAMAL_REGISTRY_PASSWORD 33 | 34 | # Inject ENV variables into containers (secrets come from .kamal/secrets). 35 | env: 36 | secret: 37 | - RAILS_MASTER_KEY 38 | clear: 39 | # Run the Solid Queue Supervisor inside the web server's Puma process to do jobs. 40 | # When you start using multiple servers, you should split out job processing to a dedicated machine. 41 | SOLID_QUEUE_IN_PUMA: true 42 | 43 | # Set number of processes dedicated to Solid Queue (default: 1) 44 | # JOB_CONCURRENCY: 3 45 | 46 | # Set number of cores available to the application on each server (default: 1). 47 | # WEB_CONCURRENCY: 2 48 | 49 | # Match this to any external database server to configure Active Record correctly 50 | # Use rails_full-db for a db accessory server on same machine via local kamal docker network. 51 | # DB_HOST: 192.168.0.2 52 | 53 | # Log everything from Rails 54 | # RAILS_LOG_LEVEL: debug 55 | 56 | # Aliases are triggered with "bin/kamal ". You can overwrite arguments on invocation: 57 | # "bin/kamal logs -r job" will tail logs from the first server in the job section. 58 | aliases: 59 | console: app exec --interactive --reuse "bin/rails console" 60 | shell: app exec --interactive --reuse "bash" 61 | logs: app logs -f 62 | dbc: app exec --interactive --reuse "bin/rails dbconsole" 63 | 64 | 65 | # Use a persistent storage volume for sqlite database files and local Active Storage files. 66 | # Recommended to change this to a mounted volume path that is backed up off server. 67 | volumes: 68 | - "rails_full_storage:/rails/storage" 69 | 70 | 71 | # Bridge fingerprinted assets, like JS and CSS, between versions to avoid 72 | # hitting 404 on in-flight requests. Combines all files from new and old 73 | # version inside the asset_path. 74 | asset_path: /rails/public/assets 75 | 76 | # Configure the image builder. 77 | builder: 78 | arch: amd64 79 | 80 | # # Build image via remote server (useful for faster amd64 builds on arm64 computers) 81 | # remote: ssh://docker@docker-builder-server 82 | # 83 | # # Pass arguments and secrets to the Docker build process 84 | # args: 85 | # RUBY_VERSION: 3.3.5 86 | # secrets: 87 | # - GITHUB_TOKEN 88 | # - RAILS_MASTER_KEY 89 | 90 | # Use a different ssh user than root 91 | # ssh: 92 | # user: app 93 | 94 | # Use accessory services (secrets come from .kamal/secrets). 95 | # accessories: 96 | # db: 97 | # image: mysql:8.0 98 | # host: 192.168.0.2 99 | # # Change to 3306 to expose port to the world instead of just local network. 100 | # port: "127.0.0.1:3306:3306" 101 | # env: 102 | # clear: 103 | # MYSQL_ROOT_HOST: '%' 104 | # secret: 105 | # - MYSQL_ROOT_PASSWORD 106 | # files: 107 | # - config/mysql/production.cnf:/etc/mysql/my.cnf 108 | # - db/production.sql:/docker-entrypoint-initdb.d/setup.sql 109 | # directories: 110 | # - data:/var/lib/mysql 111 | # redis: 112 | # image: redis:7.0 113 | # host: 192.168.0.2 114 | # port: 6379 115 | # directories: 116 | # - data:/data 117 | -------------------------------------------------------------------------------- /apps/rails-full/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /apps/rails-full/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } 24 | else 25 | config.action_controller.perform_caching = false 26 | end 27 | 28 | # Change to :null_store to avoid any caching. 29 | config.cache_store = :memory_store 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | # Make template changes take effect immediately. 38 | config.action_mailer.perform_caching = false 39 | 40 | # Set localhost to be used by links generated in mailer templates. 41 | config.action_mailer.default_url_options = { host: "localhost", port: 3000 } 42 | 43 | # Print deprecation notices to the Rails logger. 44 | config.active_support.deprecation = :log 45 | 46 | # Raise an error on page load if there are pending migrations. 47 | config.active_record.migration_error = :page_load 48 | 49 | # Highlight code that triggered database queries in logs. 50 | config.active_record.verbose_query_logs = true 51 | 52 | # Append comments with runtime information tags to SQL queries in logs. 53 | config.active_record.query_log_tags_enabled = true 54 | 55 | # Highlight code that enqueued background job in logs. 56 | config.active_job.verbose_enqueue_logs = true 57 | 58 | # Raises error for missing translations. 59 | # config.i18n.raise_on_missing_translations = true 60 | 61 | # Annotate rendered view with file names. 62 | config.action_view.annotate_rendered_view_with_filenames = true 63 | 64 | # Uncomment if you wish to allow Action Cable access from any origin. 65 | # config.action_cable.disable_request_forgery_protection = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | config.action_controller.raise_on_missing_callback_actions = true 69 | 70 | # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. 71 | # config.generators.apply_rubocop_autocorrect_after_generate! 72 | end 73 | -------------------------------------------------------------------------------- /apps/rails-full/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Turn on fragment caching in view templates. 16 | config.action_controller.perform_caching = true 17 | 18 | # Cache assets for far-future expiry since they are all digest stamped. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } 20 | 21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 22 | # config.asset_host = "http://assets.example.com" 23 | 24 | # Store uploaded files on the local file system (see config/storage.yml for options). 25 | config.active_storage.service = :local 26 | 27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 28 | config.assume_ssl = true 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # Skip http-to-https redirect for the default health check endpoint. 34 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 35 | 36 | # Log to STDOUT with the current request id as a default log tag. 37 | config.log_tags = [ :request_id ] 38 | config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) 39 | 40 | # Change to "debug" to log everything (including potentially personally-identifiable information!) 41 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 42 | 43 | # Prevent health checks from clogging up the logs. 44 | config.silence_healthcheck_path = "/up" 45 | 46 | # Don't log any deprecations. 47 | config.active_support.report_deprecations = false 48 | 49 | # Replace the default in-process memory cache store with a durable alternative. 50 | config.cache_store = :solid_cache_store 51 | 52 | # Replace the default in-process and non-durable queuing backend for Active Job. 53 | config.active_job.queue_adapter = :solid_queue 54 | config.solid_queue.connects_to = { database: { writing: :queue } } 55 | 56 | 57 | # Ignore bad email addresses and do not raise email delivery errors. 58 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 59 | # config.action_mailer.raise_delivery_errors = false 60 | 61 | # Set host to be used by links generated in mailer templates. 62 | config.action_mailer.default_url_options = { host: "example.com" } 63 | 64 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. 65 | # config.action_mailer.smtp_settings = { 66 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 67 | # password: Rails.application.credentials.dig(:smtp, :password), 68 | # address: "smtp.example.com", 69 | # port: 587, 70 | # authentication: :plain 71 | # } 72 | 73 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 74 | # the I18n.default_locale when a translation cannot be found). 75 | config.i18n.fallbacks = true 76 | 77 | # Do not dump schema after migrations. 78 | config.active_record.dump_schema_after_migration = false 79 | 80 | # Only use :id for inspections in production. 81 | config.active_record.attributes_for_inspect = [ :id ] 82 | 83 | # Enable DNS rebinding protection and other `Host` header attacks. 84 | # config.hosts = [ 85 | # "example.com", # Allow requests from example.com 86 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 87 | # ] 88 | # 89 | # Skip DNS rebinding protection for the default health check endpoint. 90 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 91 | end 92 | -------------------------------------------------------------------------------- /apps/rails-full/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = { host: "example.com" } 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | end 54 | -------------------------------------------------------------------------------- /apps/rails-full/config/importmap.rb: -------------------------------------------------------------------------------- 1 | # Pin npm packages by running ./bin/importmap 2 | 3 | pin "application" 4 | pin "@hotwired/turbo-rails", to: "turbo.min.js" 5 | pin "@hotwired/stimulus", to: "stimulus.min.js" 6 | pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" 7 | pin_all_from "app/javascript/controllers", under: "controllers" 8 | -------------------------------------------------------------------------------- /apps/rails-full/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /apps/rails-full/config/initializers/clerk.rb: -------------------------------------------------------------------------------- 1 | Clerk.configure do |config| 2 | # config.publishable_key = ENV["CLERK_PUBLISHABLE_KEY"] 3 | # config.secret_key = ENV["CLERK_SECRET_KEY"] 4 | end 5 | -------------------------------------------------------------------------------- /apps/rails-full/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /apps/rails-full/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /apps/rails-full/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /apps/rails-full/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | -------------------------------------------------------------------------------- /apps/rails-full/config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 37 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 38 | 39 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 40 | # In other environments, only set the PID file if requested. 41 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 42 | -------------------------------------------------------------------------------- /apps/rails-full/config/queue.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | dispatchers: 3 | - polling_interval: 1 4 | batch_size: 500 5 | workers: 6 | - queues: "*" 7 | threads: 3 8 | processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> 9 | polling_interval: 0.1 10 | 11 | development: 12 | <<: *default 13 | 14 | test: 15 | <<: *default 16 | 17 | production: 18 | <<: *default 19 | -------------------------------------------------------------------------------- /apps/rails-full/config/recurring.yml: -------------------------------------------------------------------------------- 1 | # production: 2 | # periodic_cleanup: 3 | # class: CleanSoftDeletedRecordsJob 4 | # queue: background 5 | # args: [ 1000, { batch_size: 500 } ] 6 | # schedule: every hour 7 | # periodic_command: 8 | # command: "SoftDeletedRecord.due.delete_all" 9 | # priority: 2 10 | # schedule: at 5am every day 11 | -------------------------------------------------------------------------------- /apps/rails-full/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | 4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 5 | # Can be used by load balancers and uptime monitors to verify that the app is live. 6 | get "up" => "rails/health#show", "as" => :rails_health_check 7 | 8 | # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) 9 | # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest 10 | # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker 11 | 12 | get "protected" => "home#protected", :as => :protected 13 | # Defines the root path route ("/") 14 | root "home#index" 15 | end 16 | -------------------------------------------------------------------------------- /apps/rails-full/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /apps/rails-full/db/cable_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_cable_messages", force: :cascade do |t| 3 | t.binary "channel", limit: 1024, null: false 4 | t.binary "payload", limit: 536870912, null: false 5 | t.datetime "created_at", null: false 6 | t.integer "channel_hash", limit: 8, null: false 7 | t.index ["channel"], name: "index_solid_cable_messages_on_channel" 8 | t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" 9 | t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /apps/rails-full/db/cache_schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema[7.2].define(version: 1) do 4 | create_table "solid_cache_entries", force: :cascade do |t| 5 | t.binary "key", limit: 1024, null: false 6 | t.binary "value", limit: 536870912, null: false 7 | t.datetime "created_at", null: false 8 | t.integer "key_hash", limit: 8, null: false 9 | t.integer "byte_size", limit: 4, null: false 10 | t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" 11 | t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" 12 | t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /apps/rails-full/db/queue_schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema[7.1].define(version: 1) do 2 | create_table "solid_queue_blocked_executions", force: :cascade do |t| 3 | t.bigint "job_id", null: false 4 | t.string "queue_name", null: false 5 | t.integer "priority", default: 0, null: false 6 | t.string "concurrency_key", null: false 7 | t.datetime "expires_at", null: false 8 | t.datetime "created_at", null: false 9 | t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" 10 | t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" 11 | t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true 12 | end 13 | 14 | create_table "solid_queue_claimed_executions", force: :cascade do |t| 15 | t.bigint "job_id", null: false 16 | t.bigint "process_id" 17 | t.datetime "created_at", null: false 18 | t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true 19 | t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" 20 | end 21 | 22 | create_table "solid_queue_failed_executions", force: :cascade do |t| 23 | t.bigint "job_id", null: false 24 | t.text "error" 25 | t.datetime "created_at", null: false 26 | t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true 27 | end 28 | 29 | create_table "solid_queue_jobs", force: :cascade do |t| 30 | t.string "queue_name", null: false 31 | t.string "class_name", null: false 32 | t.text "arguments" 33 | t.integer "priority", default: 0, null: false 34 | t.string "active_job_id" 35 | t.datetime "scheduled_at" 36 | t.datetime "finished_at" 37 | t.string "concurrency_key" 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" 41 | t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" 42 | t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" 43 | t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" 44 | t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" 45 | end 46 | 47 | create_table "solid_queue_pauses", force: :cascade do |t| 48 | t.string "queue_name", null: false 49 | t.datetime "created_at", null: false 50 | t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true 51 | end 52 | 53 | create_table "solid_queue_processes", force: :cascade do |t| 54 | t.string "kind", null: false 55 | t.datetime "last_heartbeat_at", null: false 56 | t.bigint "supervisor_id" 57 | t.integer "pid", null: false 58 | t.string "hostname" 59 | t.text "metadata" 60 | t.datetime "created_at", null: false 61 | t.string "name", null: false 62 | t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" 63 | t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true 64 | t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" 65 | end 66 | 67 | create_table "solid_queue_ready_executions", force: :cascade do |t| 68 | t.bigint "job_id", null: false 69 | t.string "queue_name", null: false 70 | t.integer "priority", default: 0, null: false 71 | t.datetime "created_at", null: false 72 | t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true 73 | t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" 74 | t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" 75 | end 76 | 77 | create_table "solid_queue_recurring_executions", force: :cascade do |t| 78 | t.bigint "job_id", null: false 79 | t.string "task_key", null: false 80 | t.datetime "run_at", null: false 81 | t.datetime "created_at", null: false 82 | t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true 83 | t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true 84 | end 85 | 86 | create_table "solid_queue_recurring_tasks", force: :cascade do |t| 87 | t.string "key", null: false 88 | t.string "schedule", null: false 89 | t.string "command", limit: 2048 90 | t.string "class_name" 91 | t.text "arguments" 92 | t.string "queue_name" 93 | t.integer "priority", default: 0 94 | t.boolean "static", default: true, null: false 95 | t.text "description" 96 | t.datetime "created_at", null: false 97 | t.datetime "updated_at", null: false 98 | t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true 99 | t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" 100 | end 101 | 102 | create_table "solid_queue_scheduled_executions", force: :cascade do |t| 103 | t.bigint "job_id", null: false 104 | t.string "queue_name", null: false 105 | t.integer "priority", default: 0, null: false 106 | t.datetime "scheduled_at", null: false 107 | t.datetime "created_at", null: false 108 | t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true 109 | t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" 110 | end 111 | 112 | create_table "solid_queue_semaphores", force: :cascade do |t| 113 | t.string "key", null: false 114 | t.integer "value", default: 1, null: false 115 | t.datetime "expires_at", null: false 116 | t.datetime "created_at", null: false 117 | t.datetime "updated_at", null: false 118 | t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" 119 | t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" 120 | t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true 121 | end 122 | 123 | add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 124 | add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 125 | add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 126 | add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 127 | add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 128 | add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade 129 | end 130 | -------------------------------------------------------------------------------- /apps/rails-full/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should ensure the existence of records required to run the application in every environment (production, 2 | # development, test). The code here should be idempotent so that it can be executed at any point in every environment. 3 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 4 | # 5 | # Example: 6 | # 7 | # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| 8 | # MovieGenre.find_or_create_by!(name: genre_name) 9 | # end 10 | -------------------------------------------------------------------------------- /apps/rails-full/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page you were looking for doesn’t exist (404 Not found) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /apps/rails-full/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-sdk-ruby/6c1a740a15c4c1a983a1e81bab63bbf5c4f9cb2d/apps/rails-full/public/icon.png -------------------------------------------------------------------------------- /apps/rails-full/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /apps/rails-full/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /apps/rails-full/test/application_system_test_case.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 4 | driven_by :selenium, using: :headless_chrome, screen_size: [ 1400, 1400 ] 5 | end 6 | -------------------------------------------------------------------------------- /apps/rails-full/test/controllers/home_controller_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class HomeControllerTest < ActionDispatch::IntegrationTest 4 | # test "the truth" do 5 | # assert true 6 | # end 7 | end 8 | -------------------------------------------------------------------------------- /apps/rails-full/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | module ActiveSupport 6 | class TestCase 7 | # Run tests in parallel with specified workers 8 | parallelize(workers: :number_of_processors) 9 | 10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /apps/sinatra/app.rb: -------------------------------------------------------------------------------- 1 | require "active_support" 2 | require "clerk/sinatra" 3 | require "sinatra/base" 4 | require "dotenv" 5 | 6 | Dotenv.load(".env") 7 | 8 | class App < Sinatra::Base 9 | register Sinatra::Clerk 10 | 11 | before "/protected*" do 12 | require_reverification! 13 | end 14 | 15 | get "/" do 16 | erb :index, format: :html5 17 | end 18 | 19 | get "/admin" do 20 | @user = clerk.user 21 | erb :index, format: :html5 22 | end 23 | 24 | get "/protected" do 25 | {message: clerk.user? ? "Valid session" : "Not logged in"}.to_json 26 | end 27 | 28 | # run! if app_file == $0 29 | end 30 | -------------------------------------------------------------------------------- /apps/sinatra/config.ru: -------------------------------------------------------------------------------- 1 | require_relative './app' 2 | 3 | Clerk.configure do |config| 4 | config.debug = true 5 | config.logger = Logger.new($stdout) 6 | end 7 | 8 | run App -------------------------------------------------------------------------------- /apps/sinatra/views/index.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sinatra 8 | 17 | 24 | 38 | 39 | 40 |

Sinatra

41 |

<%= clerk.user ? "Authenticated User: #{clerk.user.first_name} (#{clerk.user.id}) " : "Not Authenticated" %>

42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "clerk" 6 | require "byebug" 7 | 8 | # You can add fixtures and/or initialization code here to make experimenting 9 | # with your gem easier. You can also use a different console, if you like. 10 | 11 | # (If you use this, don't forget to add pry to your Gemfile!) 12 | # require "pry" 13 | # Pry.start 14 | 15 | require "irb" 16 | IRB.start(__FILE__) 17 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eo pipefail 3 | IFS=$'\n\t' 4 | 5 | # Get the version number from the generated version file 6 | VERSION=$(ruby -e "require './lib/clerk/version.rb'; puts Clerk::VERSION") 7 | 8 | echo "Building and releasing version ${VERSION}" 9 | 10 | # Install dependencies 11 | bundle install 12 | 13 | # Remove any existing gem files 14 | rm clerk-sdk-ruby-*.gem || true 15 | 16 | # Build the gem 17 | gem build clerk-sdk-ruby.gemspec 18 | 19 | # Publish to RubyGems 20 | gem push "clerk-sdk-ruby-${VERSION}.gem" 21 | 22 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /clerk-sdk-ruby.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/clerk/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "clerk-sdk-ruby" 7 | spec.version = Clerk::VERSION 8 | spec.authors = ["Clerk"] 9 | spec.email = ["ruby-sdk@clerk.dev"] 10 | 11 | spec.summary = "Clerk SDK for Ruby." 12 | spec.description = "Client SDK for the Clerk" 13 | spec.homepage = "https://github.com/clerk/clerk-sdk-ruby" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0") 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = "https://github.com/clerk/clerk-sdk-ruby" 19 | spec.metadata["changelog_uri"] = "https://github.com/clerk/clerk-sdk-ruby/blob/main/CHANGELOG.md" 20 | 21 | # Specify which files should be added to the gem when it is released. 22 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 23 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 24 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) } 25 | end 26 | spec.bindir = "exe" 27 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "faraday", ">= 1.4.1", "< 3.0" 31 | spec.add_dependency "jwt", '~> 2.5' 32 | spec.add_dependency "clerk-http-client", "~> 2.0" 33 | spec.add_dependency "concurrent-ruby", "~> 1.1" 34 | 35 | spec.add_development_dependency "byebug", "~> 11.1" 36 | spec.add_development_dependency "timecop", "~> 0.9.4" 37 | end 38 | -------------------------------------------------------------------------------- /docs/clerk-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-sdk-ruby/6c1a740a15c4c1a983a1e81bab63bbf5c4f9cb2d/docs/clerk-logo-dark.png -------------------------------------------------------------------------------- /docs/clerk-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clerk/clerk-sdk-ruby/6c1a740a15c4c1a983a1e81bab63bbf5c4f9cb2d/docs/clerk-logo-light.png -------------------------------------------------------------------------------- /lib/clerk.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "clerk/configuration" 4 | require "clerk/constants" 5 | require "clerk/error" 6 | require "clerk/sdk" 7 | require "clerk/version" 8 | 9 | if defined?(::Rails) 10 | require "clerk/rails" 11 | end 12 | 13 | module Clerk 14 | class << self 15 | def configure 16 | if block_given? 17 | yield(configuration) 18 | else 19 | configuration 20 | end 21 | end 22 | 23 | def configuration 24 | @configuration ||= Clerk::Configuration.default 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/clerk/authenticatable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/concern" 4 | 5 | module Clerk 6 | module Authenticatable 7 | extend ActiveSupport::Concern 8 | 9 | protected 10 | 11 | def clerk 12 | request.env["clerk"] 13 | end 14 | 15 | def require_reverification!(preset = StepUp::Preset::STRICT, &block) 16 | clerk.user_require_reverification!(preset) do 17 | return yield(preset) if block_given? 18 | render_reverification!(preset) 19 | end 20 | end 21 | 22 | def render_reverification!(preset = nil) 23 | render status: 403, json: StepUp::Reverification.error_payload(preset) 24 | end 25 | 26 | included do 27 | if respond_to?(:helper_method) 28 | helper_method :clerk, :require_reverification!, :render_reverification! 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/clerk/authenticate_context.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ostruct" 4 | require "forwardable" 5 | 6 | module Clerk 7 | # This class represents a parameter object used to contain all request and configuration 8 | # information required by the middleware to resolve the current request state. 9 | # link: https://refactoring.guru/introduce-parameter-object 10 | class AuthenticateContext 11 | extend Forwardable 12 | 13 | # Expose the url of the request that this parameter object was created from as a URI object. 14 | attr_reader :clerk_url 15 | 16 | # Expose properties that does not require validations or complex logic to retrieve 17 | # values by delegating them to the cookies or headers variables. 18 | def_delegators :@cookies, :session_token_in_cookie, :client_uat 19 | def_delegators :@headers, :session_token_in_header, :sec_fetch_dest 20 | 21 | # Creates a new parameter object using ::Rack::Request and Clerk::Config objects. 22 | def initialize(request, config) 23 | @clerk_url = URI.parse(request.url) 24 | @config = config 25 | 26 | @cookies = OpenStruct.new({ 27 | client_uat: request.cookies[CLIENT_UAT_COOKIE], 28 | dev_browser: request.cookies[DEV_BROWSER_COOKIE], 29 | handshake_token: request.cookies[HANDSHAKE_COOKIE], 30 | session_token_in_cookie: request.cookies[SESSION_COOKIE] 31 | }) 32 | 33 | @headers = OpenStruct.new({ 34 | accept: Utils.retrieve_header_from_request(request, ACCEPT_HEADER), 35 | host: request.host, 36 | origin: Utils.retrieve_header_from_request(request, ORIGIN_HEADER), 37 | port: request.port, 38 | sec_fetch_dest: Utils.retrieve_header_from_request(request, SEC_FETCH_DEST_HEADER), 39 | session_token_in_header: Utils.retrieve_header_from_request(request, AUTHORIZATION_HEADER).gsub(/bearer/i, "").strip 40 | }) 41 | end 42 | 43 | # The following properties are part of the props supported in all the AuthenticateContext 44 | # objects across all of our SDKs (eg JS, Go) 45 | def secret_key 46 | raise ConfigurationError, "Clerk secret key is not set" if @config.secret_key.to_s.empty? 47 | 48 | @config.secret_key.to_s 49 | end 50 | 51 | def publishable_key 52 | raise ConfigurationError, "Clerk publishable key is not set" if @config.publishable_key.to_s.to_s.empty? 53 | 54 | @config.publishable_key.to_s 55 | end 56 | 57 | def proxy_url? 58 | !proxy_url.empty? 59 | end 60 | 61 | def handshake_token 62 | @handshake_token ||= Utils.retrieve_from_query_string(@clerk_url, HANDSHAKE_COOKIE) || @cookies.handshake_token.to_s 63 | end 64 | 65 | def dev_browser 66 | @dev_browser ||= dev_browser_in_url || @cookies.dev_browser.to_s 67 | end 68 | 69 | # The frontend_api returned is without protocol prefix 70 | def frontend_api 71 | return "" unless Utils.valid_publishable_key?(publishable_key.to_s) 72 | 73 | @frontend_api ||= if proxy_url? 74 | proxy_url 75 | elsif development_instance? && !domain.empty? 76 | "clerk.#{domain}" 77 | else 78 | # remove $ postfix 79 | Utils.decode_publishable_key(publishable_key).chop.to_s 80 | end 81 | end 82 | 83 | def development_instance? 84 | secret_key.start_with?("sk_test_") 85 | end 86 | 87 | def production_instance? 88 | secret_key.start_with?("sk_live_") 89 | end 90 | 91 | def document_request? 92 | @headers.sec_fetch_dest == "document" 93 | end 94 | 95 | def accepts_html? 96 | @headers.accept&.start_with?("text/html") 97 | end 98 | 99 | def eligible_for_multi_domain? 100 | is_satellite? && document_request? && !clerk_synced? 101 | end 102 | 103 | def active_client? 104 | @cookies.client_uat.to_i.positive? 105 | end 106 | 107 | def cross_origin_request? 108 | # origin contains scheme+host and optionally port (omitted if 80 or 443) 109 | # ref. https://www.rfc-editor.org/rfc/rfc6454#section-6.1 110 | return false if @headers.origin.nil? 111 | 112 | # strip scheme 113 | origin = @headers.origin.strip.sub(%r{\A(\w+:)?//}, "") 114 | return false if origin.empty? 115 | 116 | # Rack's host and port helpers are reverse-proxy-aware; that 117 | # is, they prefer the de-facto X-Forwarded-* headers if they're set 118 | request_host = @headers.host 119 | request_host << ":#{@headers.port}" if @headers.port != 80 && @headers.port != 443 120 | 121 | origin != request_host 122 | end 123 | 124 | def dev_browser? 125 | !dev_browser.empty? 126 | end 127 | 128 | def session_token_in_header? 129 | !session_token_in_header.to_s.empty? 130 | end 131 | 132 | def handshake_token? 133 | !handshake_token.to_s.empty? 134 | end 135 | 136 | def session_token_in_cookie? 137 | !session_token_in_cookie.to_s.empty? 138 | end 139 | 140 | def dev_browser_in_url 141 | Utils.retrieve_from_query_string(@clerk_url, DEV_BROWSER_COOKIE) 142 | end 143 | 144 | def dev_browser_in_url? 145 | !!dev_browser_in_url 146 | end 147 | 148 | def domain 149 | "" # TODO: Add multi-domain support 150 | end 151 | 152 | def is_satellite? 153 | false # TODO: Add multi-domain support 154 | end 155 | 156 | def proxy_url 157 | "" # TODO: Add multi-domain support 158 | end 159 | 160 | def clerk_synced? 161 | false # TODO: Add multi-domain support 162 | end 163 | 164 | def clerk_redirect_url 165 | "" # TODO: Add multi-domain support 166 | end 167 | end 168 | end 169 | -------------------------------------------------------------------------------- /lib/clerk/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "clerk-http-client" 4 | 5 | module Clerk 6 | class Configuration 7 | attr_reader :cache_store 8 | attr_reader :debug 9 | attr_reader :logger 10 | attr_reader :excluded_routes 11 | attr_reader :publishable_key 12 | attr_reader :secret_key 13 | 14 | def initialize 15 | @excluded_routes = [] 16 | @publishable_key = ENV["CLERK_PUBLISHABLE_KEY"] 17 | @secret_key = ENV["CLERK_SECRET_KEY"] 18 | 19 | # Default to Rails.cache or ActiveSupport::Cache::MemoryStore, if available, otherwise nil 20 | @cache_store = if defined?(::Rails) 21 | ::Rails.cache 22 | elsif defined?(::ActiveSupport::Cache::MemoryStore) 23 | ::ActiveSupport::Cache::MemoryStore.new 24 | end 25 | 26 | ClerkHttpClient.configure do |config| 27 | unless secret_key.nil? || secret_key.empty? 28 | config.access_token = @secret_key 29 | end 30 | end 31 | end 32 | 33 | def self.default 34 | @@default ||= new 35 | end 36 | 37 | def update(options) 38 | options.each do |key, value| 39 | send(:"#{key}=", value) 40 | end 41 | end 42 | 43 | def debug=(value) 44 | ClerkHttpClient::Configuration.default.debugging = value 45 | @debug = value 46 | end 47 | 48 | def cache_store=(store) 49 | if !store 50 | @cache_store = nil 51 | return 52 | end 53 | 54 | raise ArgumentError, "cache_store must respond to :fetch" unless store.respond_to?(:fetch) 55 | 56 | @cache_store = store 57 | end 58 | 59 | def excluded_routes=(routes) 60 | raise ArgumentError, "excluded_routes must be an array" unless routes.is_a?(Array) 61 | raise ArgumentError, "All elements in the excluded_routes array must be strings" unless routes.all? { |r| r.is_a?(String) } 62 | 63 | @excluded_routes = routes 64 | end 65 | 66 | def publishable_key=(pk) 67 | raise ArgumentError, "publishable_key must start with 'pk_'" unless pk.start_with?("pk_") 68 | 69 | @publishable_key = pk 70 | end 71 | 72 | def secret_key=(sk) 73 | raise ArgumentError, "secret_key must start with 'sk_'" unless sk.start_with?("sk_") 74 | 75 | ClerkHttpClient::Configuration.default.access_token = sk 76 | @secret_key = sk 77 | end 78 | 79 | def logger=(logger) 80 | ClerkHttpClient::Configuration.default.logger = logger 81 | @logger = logger 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/clerk/constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Clerk 4 | SESSION_COOKIE = "__session" 5 | CLIENT_UAT_COOKIE = "__client_uat" 6 | 7 | # Dev Browser 8 | DEV_BROWSER_COOKIE = "__clerk_db_jwt" 9 | 10 | # Handshake 11 | HANDSHAKE_COOKIE = "__clerk_handshake" 12 | HANDSHAKE_COOKIE_DIRECTIVES_KEY = "handshake" 13 | 14 | # auth debug response headers 15 | AUTH_STATUS_HEADER = "x-clerk-auth-status" 16 | AUTH_REASON_HEADER = "x-clerk-auth-reason" 17 | AUTH_MESSAGE_HEADER = "x-clerk-auth-message" 18 | 19 | SEC_FETCH_DEST_HEADER = "HTTP_SEC_FETCH_DEST" 20 | 21 | # headers used in response - should be lowered case and without http prefix 22 | ACCESS_CONTROL_ALLOW_CREDENTIALS_HEADER = "access-control-allow-credentials" 23 | ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "access-control-allow-origin" 24 | CONTENT_TYPE_HEADER = "content-type" 25 | LOCATION_HEADER = "location" 26 | SET_COOKIE_HEADER = "set-cookie" 27 | 28 | # clerk url related headers 29 | AUTHORIZATION_HEADER = "HTTP_AUTHORIZATION" 30 | ACCEPT_HEADER = "HTTP_ACCEPT" 31 | USER_AGENT_HEADER = "HTTP_USER_AGENT" 32 | ORIGIN_HEADER = "HTTP_ORIGIN" 33 | 34 | module TokenVerificationErrorReason 35 | TOKEN_INVALID = "token-invalid" 36 | TOKEN_EXPIRED = "token-expired" 37 | TOKEN_NOT_ACTIVE_YET = "token-not-active-yet" 38 | JWK_FAILED_TO_RESOLVE = "jwk-failed-to-resolve" 39 | end 40 | 41 | module AuthErrorReason 42 | CLIENT_UAT_WITHOUT_SESSION_TOKEN = "client-uat-but-no-session-token" 43 | DEV_BROWSER_SYNC = "dev-browser-sync" 44 | DEV_BROWSER_MISSING = "dev-browser-missing" 45 | PRIMARY_RESPONDS_TO_SYNCING = "primary-responds-to-syncing" 46 | SATELLITE_COOKIE_NEEDS_SYNCING = "satellite-needs-syncing" 47 | SESSION_TOKEN_AND_UAT_MISSING = "session-token-and-uat-missing" 48 | SESSION_TOKEN_MISSING = "session-token-missing" 49 | SESSION_TOKEN_OUTDATED = "session-token-outdated" 50 | SESSION_TOKEN_WITHOUT_CLIENT_UAT = "session-token-but-no-client-uat" 51 | UNEXPECTED_ERROR = "unexpected-error" 52 | end 53 | 54 | module StepUp 55 | module Preset 56 | STRICT_MFA = {after_minutes: 10, level: :multi_factor} 57 | STRICT = {after_minutes: 10, level: :second_factor} 58 | MODERATE = {after_minutes: 60, level: :second_factor} 59 | LAX = {after_minutes: 1440, level: :second_factor} 60 | end 61 | 62 | module Reverification 63 | def self.error_payload(missing_config) 64 | { 65 | clerk_error: { 66 | type: "forbidden", 67 | reason: "reverification-error", 68 | metadata: {reverification: missing_config} 69 | } 70 | } 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/clerk/error.rb: -------------------------------------------------------------------------------- 1 | module Clerk 2 | class Error < StandardError 3 | attr_reader :status 4 | 5 | def initialize(msg, status:) 6 | @errors = msg["errors"] 7 | @status = status 8 | super(msg.merge(status: status)) 9 | end 10 | end 11 | 12 | class AuthenticationError < Error; end 13 | 14 | class ConfigurationError < StandardError; end 15 | 16 | class FatalError < Error; end 17 | end 18 | -------------------------------------------------------------------------------- /lib/clerk/jwks_cache.rb: -------------------------------------------------------------------------------- 1 | require "concurrent" 2 | 3 | module Clerk 4 | class JWKSCache 5 | def initialize(lifetime) 6 | @lifetime = lifetime 7 | @jwks = nil 8 | @last_update = nil 9 | @lock = Concurrent::ReadWriteLock.new 10 | end 11 | 12 | def fetch(sdk, force_refresh: false, kid_not_found: false) 13 | should_refresh = @lock.with_read_lock do 14 | now = Time.now.to_i 15 | 16 | @jwks.nil? || @last_update.nil? || force_refresh || 17 | (now - @last_update > @lifetime) || 18 | (kid_not_found && now - @last_update > 300) 19 | end 20 | 21 | if should_refresh 22 | @lock.with_write_lock do 23 | @last_update = Time.now.to_i 24 | @jwks = begin 25 | sdk.jwks.get_jwks.keys.map(&:to_hash) 26 | rescue Clerk::Error, ClerkHttpClient::ApiError 27 | nil 28 | end 29 | end 30 | end 31 | 32 | @lock.with_read_lock do 33 | @jwks 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/clerk/proxy.rb: -------------------------------------------------------------------------------- 1 | require "clerk" 2 | require "clerk/authenticate_context" 3 | require "clerk/authenticate_request" 4 | 5 | module Clerk 6 | class Proxy 7 | CACHE_TTL = 60 # seconds 8 | 9 | attr_reader :session_claims, :session_token 10 | 11 | def initialize(session_claims: nil, session_token: nil) 12 | @session_claims = session_claims 13 | @session_token = session_token 14 | end 15 | 16 | def user? 17 | !@session_claims.nil? 18 | end 19 | 20 | def user 21 | return nil unless user? 22 | 23 | @user ||= fetch_user(user_id) 24 | end 25 | 26 | def user_id 27 | return nil unless user? 28 | 29 | @session_claims["sub"] 30 | end 31 | 32 | def organization? 33 | !organization_id.nil? 34 | end 35 | 36 | def organization 37 | return nil unless organization? 38 | 39 | @org ||= fetch_org(organization_id) 40 | end 41 | 42 | def organization_id 43 | return nil unless user? 44 | 45 | @session_claims["org_id"] 46 | end 47 | 48 | def organization_role 49 | return nil if @session_claims.nil? 50 | 51 | @session_claims["org_role"] 52 | end 53 | 54 | def organization_permissions 55 | return nil if @session_claims.nil? 56 | 57 | @session_claims["org_permissions"] 58 | end 59 | 60 | # Returns true if the session needs to perform step up verification 61 | def user_reverified?(params) 62 | return false unless user? 63 | 64 | fva = session_claims["fva"] 65 | 66 | # the feature is disabled 67 | return true if fva.nil? 68 | 69 | level = params[:level] 70 | after_minutes = params[:after_minutes].to_i 71 | 72 | return false if after_minutes.nil? || level.nil? 73 | 74 | factor1_age, factor2_age = fva 75 | is_valid_factor1 = factor1_age != -1 && after_minutes > factor1_age 76 | is_valid_factor2 = factor2_age != -1 && after_minutes > factor2_age 77 | 78 | case level 79 | when :first_factor 80 | is_valid_factor1 81 | when :second_factor 82 | (factor2_age == -1) ? is_valid_factor1 : is_valid_factor2 83 | when :multi_factor 84 | (factor2_age == -1) ? is_valid_factor1 : is_valid_factor1 && is_valid_factor2 85 | end 86 | end 87 | 88 | def user_needs_reverification?(preset = StepUp::Preset::STRICT) 89 | !user_reverified?(preset) 90 | end 91 | 92 | def user_require_reverification!(preset = StepUp::Preset::STRICT, &block) 93 | return unless user_needs_reverification?(preset) 94 | yield(preset) if block_given? 95 | end 96 | 97 | def user_reverification_rack_response(config = nil) 98 | raise ArgumentError, "Missing config, please pass a preset a la `Clerk::StepUp::Preset::*`" if config.nil? 99 | 100 | [ 101 | 403, 102 | {Clerk::CONTENT_TYPE_HEADER => "application/json"}, 103 | [StepUp::Reverification.error_payload(config).to_json] 104 | ] 105 | end 106 | 107 | private 108 | 109 | def fetch_user(user_id) 110 | cached_fetch("clerk:user:#{user_id}") do 111 | sdk.users.get_user(user_id) 112 | end 113 | end 114 | 115 | def fetch_org(org_id) 116 | cached_fetch("clerk:org:#{org_id}") do 117 | sdk.organizations.get_organization(org_id) 118 | end 119 | end 120 | 121 | def cached_fetch(key, &block) 122 | store = Clerk.configuration.cache_store 123 | 124 | if store 125 | store.fetch(key, expires_in: CACHE_TTL, &block) 126 | else 127 | yield 128 | end 129 | end 130 | 131 | def sdk 132 | @sdk ||= Clerk::SDK.new 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /lib/clerk/rack.rb: -------------------------------------------------------------------------------- 1 | require "clerk" 2 | require "clerk/rack_middleware" 3 | -------------------------------------------------------------------------------- /lib/clerk/rack_middleware.rb: -------------------------------------------------------------------------------- 1 | require "clerk" 2 | require "clerk/authenticate_context" 3 | require "clerk/authenticate_request" 4 | require "clerk/proxy" 5 | require "clerk/utils" 6 | 7 | module Clerk 8 | module Rack 9 | class Middleware 10 | def initialize(app, options = {}) 11 | @app = app 12 | 13 | Clerk.configuration.update(options) if options 14 | @excluded_routes, @excluded_routes_wildcards = Clerk::Utils.filter_routes(Clerk.configuration.excluded_routes) 15 | end 16 | 17 | def call(env) 18 | env["clerk.initialized"] = true 19 | 20 | req = ::Rack::Request.new(env) 21 | 22 | if @excluded_routes[req.path] 23 | env["clerk.excluded_route"] = true 24 | return @app.call(env) 25 | end 26 | 27 | @excluded_routes_wildcards.each do |route| 28 | if req.path.start_with?(route) 29 | env["clerk.excluded_route"] = true 30 | return @app.call(env) 31 | end 32 | end 33 | 34 | env["clerk"] = Clerk::Proxy.new 35 | 36 | auth_context = AuthenticateContext.new(req, Clerk.configuration) 37 | auth_request = AuthenticateRequest.new(auth_context) 38 | 39 | status, auth_request_headers, body = auth_request.resolve(env) 40 | 41 | return [status, auth_request_headers, body] if status 42 | 43 | status, headers, body = @app.call(env) 44 | 45 | unless auth_request_headers.empty? 46 | # Remove them to avoid overriding existing cookies set in headers by other middlewares 47 | auth_request_cookies = auth_request_headers.delete(SET_COOKIE_HEADER.downcase) 48 | # merge non-cookie related headers into response headers 49 | headers.merge!(auth_request_headers) 50 | 51 | set_cookie_headers!(headers, auth_request_cookies) if auth_request_cookies 52 | end 53 | 54 | [status, headers, body] 55 | end 56 | 57 | private 58 | 59 | def parse_cookie_key(cookie_header) 60 | cookie_header.split(";")[0].split("=")[0] 61 | end 62 | 63 | def set_cookie_headers!(headers, cookie_headers) 64 | cookie_headers.each do |cookie_header| 65 | cookie_key = parse_cookie_key(cookie_header) 66 | cookie = ::Clerk::Utils.parse_cookies_header(cookie_header) 67 | cookie_params = convert_http_cookie_to_cookie_setter_params(cookie_key, cookie) 68 | ::Rack::Utils.set_cookie_header!(headers, cookie_key, cookie_params) 69 | end 70 | end 71 | 72 | def convert_http_cookie_to_cookie_setter_params(cookie_key, cookie) 73 | # convert cookie to to match cookie setter method params (lowercase symbolized keys with `:value` key) 74 | cookie_params = cookie.transform_keys { |k| k.downcase.to_sym } 75 | # drop the current cookie name key to avoid polluting the expected cookie params 76 | cookie_params[:value] = cookie_params.delete(cookie_key.to_sym) 77 | 78 | # Ensure secure and httponly are set to true if present 79 | cookie_params[:secure] = cookie_params.has_key?(:secure) 80 | cookie_params[:httponly] = cookie_params.has_key?(:httponly) 81 | 82 | # fix issue with cookie expiration expected to be Date type 83 | cookie_params[:expires] = Date.parse(cookie_params[:expires]) if cookie_params[:expires] 84 | 85 | cookie_params 86 | end 87 | end 88 | 89 | class Reverification 90 | def initialize(app, routes: ["/*"], preset: Clerk::StepUp::Preset::STRICT) 91 | @app = app 92 | @preset = preset 93 | 94 | @included_routes, @included_routes_wildcards = Clerk::Utils.filter_routes(routes) 95 | end 96 | 97 | def call(env) 98 | raise Clerk::ConfigurationError, "`Clerk::Rack::Reverification` must be initialized after `Clerk::Rack::Middleware`" unless env["clerk.initialized"] 99 | return @app.call(env) if env["clerk.excluded_route"] 100 | 101 | req = ::Rack::Request.new(env) 102 | valid_route = @included_routes[req.path] || @included_routes_wildcards.any? { |route| req.path.start_with?(route) } 103 | 104 | if valid_route && env["clerk"].user_needs_reverification?(@preset) 105 | return env["clerk"].user_reverification_rack_response(@preset) 106 | end 107 | 108 | @app.call(env) 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/clerk/rails.rb: -------------------------------------------------------------------------------- 1 | require "clerk" 2 | require "clerk/authenticatable" 3 | require "clerk/railtie" if defined?(Rails::Railtie) 4 | -------------------------------------------------------------------------------- /lib/clerk/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "clerk/rack_middleware" 4 | 5 | module Clerk 6 | module Rails 7 | class Railtie < ::Rails::Railtie 8 | initializer "clerk.configure_rails_initialization" do |app| 9 | unless ENV["CLERK_SKIP_RAILTIE"] 10 | app.middleware.use Clerk::Rack::Middleware 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/clerk/sdk.rb: -------------------------------------------------------------------------------- 1 | require "clerk-http-client" 2 | require "clerk/jwks_cache" 3 | require "clerk/version" 4 | require "jwt" 5 | 6 | module Clerk 7 | class SDK < ClerkHttpClient::SDK 8 | DEFAULT_HEADERS = { 9 | "User-Agent": "Clerk/#{Clerk::VERSION}; Faraday/#{Faraday::VERSION}; Ruby/#{RUBY_VERSION}", 10 | "X-Clerk-SDK": "ruby/#{Clerk::VERSION}", 11 | "Clerk-API-Version": "2025-04-10", 12 | } 13 | 14 | # How often (in seconds) should JWKs be refreshed 15 | JWKS_CACHE_LIFETIME = 3600 # 1 hour 16 | 17 | @@jwks_cache = JWKSCache.new(JWKS_CACHE_LIFETIME) 18 | 19 | def self.jwks_cache 20 | @@jwks_cache 21 | end 22 | 23 | # Returns the decoded JWT payload without verifying if the signature is valid. 24 | # 25 | # WARNING: This will not verify whether the signature is valid. You should not 26 | # use this for untrusted messages! You most likely want to use `verify_token`. 27 | def decode_token(token) 28 | JWT.decode(token, nil, false).first 29 | end 30 | 31 | # Decode the JWT and verify it's valid (verify claims, signature etc.) using the provided algorithms. 32 | # 33 | # JWKS are cached for JWKS_CACHE_LIFETIME seconds, in order to avoid unecessary roundtrips. 34 | # In order to invalidate the cache, pass `force_refresh_jwks: true`. 35 | # 36 | # A timeout for the request to the JWKs endpoint can be set with the `timeout` argument. 37 | def verify_token(token, force_refresh_jwks: false, algorithms: ["RS256"], timeout: 5) 38 | jwk_loader = ->(options) do 39 | # JWT.decode requires that the 'keys' key in the Hash is a symbol (as 40 | # opposed to a string which our SDK returns by default) 41 | {keys: SDK.jwks_cache.fetch(self, kid_not_found: options[:invalidate] || options[:kid_not_found], force_refresh: force_refresh_jwks)} 42 | end 43 | 44 | claims = JWT.decode(token, nil, true, algorithms: algorithms, exp_leeway: timeout, jwks: jwk_loader).first 45 | 46 | # orgs 47 | if claims["v"].nil? || claims["v"] == 1 48 | claims["v"] = 1 49 | elsif claims["v"] == 2 && claims["o"] 50 | claims["org_id"] = claims["o"].fetch("id", nil) 51 | claims["org_slug"] = claims["o"].fetch("slg", nil) 52 | claims["org_role"] = "org:#{claims["o"].fetch("rol", nil)}" 53 | 54 | org_permissions = compute_org_permissions_from_v2_token(claims) 55 | claims["org_permissions"] = org_permissions if org_permissions.any? 56 | claims.delete("o") 57 | claims.delete("fea") 58 | end 59 | 60 | claims 61 | end 62 | 63 | private 64 | 65 | def compute_org_permissions_from_v2_token(claims) 66 | features = claims["fea"] ? claims["fea"].split(",") : [] 67 | permissions = claims["o"]["per"] ? claims["o"]["per"].split(",") : [] 68 | mappings = claims["o"]["fpm"] ? claims["o"]["fpm"].split(",") : [] 69 | org_permissions = [] 70 | 71 | mappings.each_with_index do |mapping, i| 72 | scope, feature = features[i].split(":") 73 | 74 | next if !scope.include?("o") # not an orgs-related permission 75 | 76 | mapping.to_i.to_s(2).reverse.each_char.each_with_index do |bit, i| 77 | org_permissions << "org:#{feature}:#{permissions[i]}" if bit == "1" 78 | end 79 | end 80 | 81 | org_permissions 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/clerk/sinatra.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/base" 2 | require "clerk/rack" 3 | 4 | module Sinatra 5 | module Clerk 6 | module Helpers 7 | def clerk 8 | env["clerk"] 9 | end 10 | 11 | def require_reverification!(preset = ::Clerk::StepUp::Preset::STRICT, &block) 12 | clerk.user_require_reverification!(preset) do 13 | return yield(preset) if block_given? 14 | render_reverification!(preset) 15 | end 16 | end 17 | 18 | def render_reverification!(preset = nil) 19 | halt 403, ::Clerk::StepUp::Reverification.error_payload(preset).to_json 20 | end 21 | 22 | def clerk_sdk 23 | @@sdk ||= ::Clerk::SDK.new 24 | end 25 | end 26 | 27 | def self.registered(app) 28 | app.helpers Clerk::Helpers 29 | app.use ::Clerk::Rack::Middleware 30 | 31 | app.set(:auth) do |active| 32 | condition do 33 | redirect clerk.sign_in_url if active && !clerk.session 34 | end 35 | end 36 | 37 | app.set(:reverify) do |preset| 38 | condition do 39 | if preset === true 40 | preset = ::Clerk::StepUp::Preset::STRICT 41 | end 42 | 43 | if preset 44 | require_reverification!(preset) 45 | end 46 | end 47 | end 48 | end 49 | end 50 | 51 | register Clerk 52 | end 53 | -------------------------------------------------------------------------------- /lib/clerk/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | 5 | module Clerk 6 | module Utils 7 | class << self 8 | def decode_publishable_key(publishable_key) 9 | Base64.decode64(publishable_key.split("_")[2].to_s) 10 | end 11 | 12 | def filter_routes(routes) 13 | filtered_routes = {} 14 | filtered_wildcard_routes = [] 15 | 16 | routes.each do |route| 17 | route = route.strip 18 | 19 | if route.end_with?("/*") 20 | filtered_wildcard_routes << route[0..-2] 21 | else 22 | filtered_routes[route] = true 23 | end 24 | end 25 | 26 | filtered_wildcard_routes.uniq! 27 | 28 | [filtered_routes, filtered_wildcard_routes] 29 | end 30 | 31 | def retrieve_header_from_request(request, key) 32 | (request.env[key] || request.env[key.downcase]).to_s 33 | end 34 | 35 | def retrieve_from_query_string(url, key) 36 | ::Rack::Utils.parse_query(url.query)[key] 37 | end 38 | 39 | def valid_publishable_key?(publishable_key) 40 | raise ArgumentError, "publishable_key must be a string" unless publishable_key.is_a?(String) 41 | 42 | key = publishable_key.to_s 43 | valid_publishable_key_prefix?(key) && valid_publishable_key_postfix?(key) 44 | end 45 | 46 | def valid_publishable_key_postfix?(publishable_key) 47 | decode_publishable_key(publishable_key).end_with?("$") 48 | end 49 | 50 | def valid_publishable_key_prefix?(publishable_key) 51 | publishable_key.start_with?("pk_live_", "pk_test_") 52 | end 53 | 54 | # NOTE: This is a copy of Rack::Utils.parse_cookies_header to allow for 55 | # compatibility with older versions of Rack. 56 | def parse_cookies_header(value) 57 | return {} unless value 58 | 59 | value.split(/; */n).each_with_object({}) do |cookie, cookies| 60 | next if cookie.empty? 61 | key, value = cookie.split('=', 2) 62 | cookies[key] = (unescape(value) rescue value) unless cookies.key?(key) 63 | end 64 | end 65 | 66 | private 67 | 68 | def unescape(s, encoding = Encoding::UTF_8) 69 | URI.decode_www_form_component(s, encoding) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/clerk/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Clerk 4 | VERSION = "4.2.1" 5 | end 6 | -------------------------------------------------------------------------------- /spec/clerk/authenticatable_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "clerk/authenticatable" 5 | 6 | RSpec.describe Clerk::Authenticatable do 7 | let(:klass) do 8 | Class.new do 9 | include Clerk::Authenticatable 10 | 11 | attr_reader :request 12 | 13 | def initialize(request) 14 | @request = request 15 | end 16 | 17 | def render(options = {}) 18 | @render_options = options 19 | end 20 | 21 | attr_reader :render_options 22 | end 23 | end 24 | 25 | let(:clerk_client) { instance_double("ClerkClient") } 26 | let(:request) { double("Request", env: {"clerk" => clerk_client}) } 27 | let(:controller) { klass.new(request) } 28 | 29 | describe "#clerk" do 30 | it "returns the clerk client from request.env" do 31 | expect(controller.send(:clerk)).to eq(clerk_client) 32 | end 33 | end 34 | 35 | describe "#require_reverification!" do 36 | let(:preset) { Clerk::StepUp::Preset::STRICT } 37 | 38 | context "when block is given" do 39 | it "yields to the block when reverification is required" do 40 | expect(clerk_client).to receive(:user_require_reverification!).with(preset).and_yield 41 | 42 | block_called = false 43 | controller.send(:require_reverification!, preset) { block_called = true } 44 | 45 | expect(block_called).to be true 46 | end 47 | end 48 | 49 | context "when no block is given" do 50 | it "renders reverification error" do 51 | expect(clerk_client).to receive(:user_require_reverification!).with(preset).and_yield 52 | 53 | controller.send(:require_reverification!, preset) 54 | 55 | expect(controller.render_options).to eq({ 56 | status: 403, 57 | json: Clerk::StepUp::Reverification.error_payload(preset) 58 | }) 59 | end 60 | end 61 | end 62 | 63 | describe "#render_reverification!" do 64 | let(:preset) { Clerk::StepUp::Preset::STRICT } 65 | let(:error_payload) { Clerk::StepUp::Reverification.error_payload(preset) } 66 | 67 | before do 68 | allow(Clerk::StepUp::Reverification).to receive(:error_payload).with(preset).and_return(error_payload) 69 | end 70 | 71 | it "renders error payload with 403 status" do 72 | controller.send(:render_reverification!, preset) 73 | 74 | expect(controller.render_options).to eq({ 75 | status: 403, 76 | json: error_payload 77 | }) 78 | end 79 | end 80 | 81 | describe "helper_method registration" do 82 | it "registers clerk helper methods" do 83 | expect(klass.class).to receive(:helper_method).with(:clerk, :require_reverification!, :render_reverification!) 84 | klass.class.send(:include, described_class) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/clerk/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "logger" 5 | 6 | RSpec.describe Clerk::Configuration do 7 | let(:valid_pk) { "pk_test_key" } 8 | let(:valid_sk) { "sk_test_key" } 9 | 10 | describe "#initialize" do 11 | it "initializes with default values" do 12 | with_modified_env(CLERK_PUBLISHABLE_KEY: valid_pk, CLERK_SECRET_KEY: valid_sk) do 13 | config = described_class.new 14 | 15 | expect(config.excluded_routes).to eq([]) 16 | expect(config.publishable_key).to eq(ENV["CLERK_PUBLISHABLE_KEY"]) 17 | expect(config.secret_key).to eq(ENV["CLERK_SECRET_KEY"]) 18 | expect(config.cache_store).to be_nil 19 | expect(config.logger).to be_nil 20 | end 21 | end 22 | 23 | it "initializes ClerkHttpClient with the secret key" do 24 | with_modified_env(CLERK_SECRET_KEY: valid_sk) do 25 | described_class.new 26 | expect(ClerkHttpClient::Configuration.default.access_token).to eq(ENV["CLERK_SECRET_KEY"]) 27 | end 28 | end 29 | 30 | context "cache store defaults" do 31 | context "when ::Rails is defined" do 32 | before do 33 | stub_const("Rails", Class.new) 34 | allow(Rails).to receive(:cache) 35 | end 36 | 37 | it "uses Rails.cache as the default cache store" do 38 | expect(described_class.new.cache_store).to eq(Rails.cache) 39 | end 40 | end 41 | 42 | context "when ::ActiveSupport::Cache::MemoryStore is defined" do 43 | before do 44 | stub_const("ActiveSupport::Cache::MemoryStore", Class.new) 45 | end 46 | 47 | it "uses ActiveSupport::Cache::MemoryStore as the default cache store" do 48 | expect(described_class.new.cache_store).to be_an_instance_of(ActiveSupport::Cache::MemoryStore) 49 | end 50 | end 51 | 52 | context "when neither is defined" do 53 | it "returns nil" do 54 | expect(described_class.new.cache_store).to be_nil 55 | end 56 | end 57 | end 58 | end 59 | 60 | describe "#update" do 61 | it "updates multiple configuration options" do 62 | config = described_class.new 63 | options = { 64 | publishable_key: valid_pk, 65 | secret_key: valid_sk, 66 | excluded_routes: ["/health"] 67 | } 68 | 69 | config.update(options) 70 | 71 | expect(config.publishable_key).to eq(valid_pk) 72 | expect(config.secret_key).to eq(valid_sk) 73 | expect(config.excluded_routes).to eq(["/health"]) 74 | end 75 | end 76 | 77 | describe "#cache_store=" do 78 | let(:valid_cache) { double("cache_store", fetch: nil) } 79 | 80 | it "accepts a valid cache store" do 81 | config = described_class.new 82 | config.cache_store = valid_cache 83 | expect(config.cache_store).to eq(valid_cache) 84 | end 85 | 86 | it "allows setting to nil" do 87 | config = described_class.new 88 | config.cache_store = nil 89 | expect(config.cache_store).to be_nil 90 | end 91 | 92 | it "raises error when cache store doesn't respond to fetch" do 93 | config = described_class.new 94 | invalid_cache = double("invalid_cache") 95 | 96 | expect { config.cache_store = invalid_cache } 97 | .to raise_error(ArgumentError, "cache_store must respond to :fetch") 98 | end 99 | end 100 | 101 | describe "#excluded_routes=" do 102 | it "accepts an array of strings" do 103 | config = described_class.new 104 | routes = ["/health", "/status"] 105 | 106 | config.excluded_routes = routes 107 | expect(config.excluded_routes).to eq(routes) 108 | end 109 | 110 | it "raises error when input is not an array" do 111 | config = described_class.new 112 | 113 | expect { config.excluded_routes = "not_an_array" } 114 | .to raise_error(ArgumentError, "excluded_routes must be an array") 115 | end 116 | 117 | it "raises error when array contains non-string elements" do 118 | config = described_class.new 119 | 120 | expect { config.excluded_routes = ["/health", 123] } 121 | .to raise_error(ArgumentError, "All elements in the excluded_routes array must be strings") 122 | end 123 | end 124 | 125 | describe "#publishable_key=" do 126 | it "accepts a valid publishable key" do 127 | config = described_class.new 128 | config.publishable_key = valid_pk 129 | expect(config.publishable_key).to eq(valid_pk) 130 | end 131 | 132 | it "raises error when key doesn't start with 'pk_'" do 133 | config = described_class.new 134 | 135 | expect { config.publishable_key = "invalid_key" } 136 | .to raise_error(ArgumentError, "publishable_key must start with 'pk_'") 137 | end 138 | end 139 | 140 | describe "#secret_key=" do 141 | it "accepts a valid secret key" do 142 | config = described_class.new 143 | config.secret_key = valid_sk 144 | expect(config.secret_key).to eq(valid_sk) 145 | end 146 | 147 | it "raises error when key doesn't start with 'sk_'" do 148 | config = described_class.new 149 | 150 | expect { config.secret_key = "invalid_key" } 151 | .to raise_error(ArgumentError, "secret_key must start with 'sk_'") 152 | end 153 | 154 | it "updates ClerkHttpClient configuration" do 155 | config = described_class.new 156 | expect(ClerkHttpClient::Configuration.default) 157 | .to receive(:access_token=) 158 | .with(valid_sk) 159 | 160 | config.secret_key = valid_sk 161 | end 162 | end 163 | 164 | describe "#debug=" do 165 | it "updates debug flag and ClerkHttpClient configuration" do 166 | config = described_class.new 167 | expect(ClerkHttpClient::Configuration.default) 168 | .to receive(:debugging=) 169 | .with(true) 170 | 171 | config.debug = true 172 | expect(config.debug).to be true 173 | end 174 | end 175 | 176 | describe "#logger=" do 177 | it "updates logger ClerkHttpClient configuration" do 178 | config = described_class.new 179 | logger = Logger.new($stdout) 180 | expect(ClerkHttpClient::Configuration.default) 181 | .to receive(:logger=) 182 | .with(logger) 183 | 184 | config.logger = logger 185 | expect(config.logger).to be_a(Logger) 186 | end 187 | end 188 | 189 | describe ".default" do 190 | it "returns the same instance on multiple calls" do 191 | instance1 = described_class.default 192 | instance2 = described_class.default 193 | 194 | expect(instance1).to be(instance2) 195 | end 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /spec/clerk/error_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Clerk::Error do 4 | describe "#initialize" do 5 | let(:error_msg) { {"errors" => ["Invalid token"]} } 6 | let(:status) { 401 } 7 | 8 | subject(:error) { described_class.new(error_msg, status: status) } 9 | 10 | it "sets the status" do 11 | expect(error.status).to eq(status) 12 | end 13 | 14 | it "includes status in the error message" do 15 | expect(error.message).to include("status=>401") 16 | end 17 | end 18 | end 19 | 20 | RSpec.describe Clerk::AuthenticationError do 21 | it "inherits from Clerk::Error" do 22 | expect(described_class.superclass).to eq(Clerk::Error) 23 | end 24 | end 25 | 26 | RSpec.describe Clerk::ConfigurationError do 27 | it "inherits from StandardError" do 28 | expect(described_class.superclass).to eq(StandardError) 29 | end 30 | end 31 | 32 | RSpec.describe Clerk::FatalError do 33 | it "inherits from Clerk::Error" do 34 | expect(described_class.superclass).to eq(Clerk::Error) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/clerk/jwks_cache_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Clerk::JWKSCache do 4 | let(:lifetime) { 3600 } # 1 hour cache lifetime 5 | let(:cache) { described_class.new(lifetime) } 6 | let(:sdk) { Clerk::SDK.new } 7 | let(:jwks_api) { instance_double("ClerkHttpClient::JWKSApi") } 8 | let(:mock_keys) { [double(to_hash: {kid: "key1"}), double(to_hash: {kid: "key2"})] } 9 | let(:expected_result) { mock_keys.map(&:to_hash) } 10 | 11 | before do 12 | allow(sdk).to receive(:jwks).and_return(jwks_api) 13 | allow(jwks_api).to receive(:get_jwks).and_return(double(keys: mock_keys)) 14 | end 15 | 16 | describe "#fetch" do 17 | it "fetches and caches JWKS on first call" do 18 | result = cache.fetch(sdk) 19 | expect(result).to eq(expected_result) 20 | end 21 | 22 | it "returns cached result on subsequent calls within lifetime" do 23 | first_result = cache.fetch(sdk) 24 | expect(jwks_api).to have_received(:get_jwks).once 25 | 26 | second_result = cache.fetch(sdk) 27 | expect(second_result).to eq(first_result) 28 | expect(jwks_api).to have_received(:get_jwks).once 29 | end 30 | 31 | it "refreshes cache when force_refresh is true" do 32 | cache.fetch(sdk) 33 | expect(jwks_api).to have_received(:get_jwks).once 34 | 35 | cache.fetch(sdk, force_refresh: true) 36 | expect(jwks_api).to have_received(:get_jwks).twice 37 | end 38 | 39 | it "refreshes cache when lifetime has expired" do 40 | cache.fetch(sdk) 41 | expect(jwks_api).to have_received(:get_jwks).once 42 | 43 | allow(Time).to receive(:now).and_return(Time.now + lifetime + 1) 44 | 45 | cache.fetch(sdk) 46 | expect(jwks_api).to have_received(:get_jwks).twice 47 | end 48 | 49 | it "refreshes cache when kid_not_found is true and last update was over 5 minutes ago" do 50 | cache.fetch(sdk) 51 | expect(jwks_api).to have_received(:get_jwks).once 52 | 53 | allow(Time).to receive(:now).and_return(Time.now + 301) # 5 minutes + 1 second 54 | 55 | cache.fetch(sdk, kid_not_found: true) 56 | expect(jwks_api).to have_received(:get_jwks).twice 57 | end 58 | 59 | it "returns nil when API call fails" do 60 | allow(jwks_api).to receive(:get_jwks).and_raise(ClerkHttpClient::ApiError) 61 | 62 | result = cache.fetch(sdk) 63 | expect(result).to be_nil 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/clerk/proxy_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | RSpec.describe Clerk::Proxy do 4 | let(:user_id) { "user_123" } 5 | let(:org_id) { "org_456" } 6 | let(:session_claims) do 7 | { 8 | "sub" => user_id, 9 | "org_id" => org_id, 10 | "org_role" => "admin", 11 | "org_permissions" => ["read", "write"], 12 | "fva" => [10, 5] 13 | } 14 | end 15 | let(:session_token) { "test_token" } 16 | let(:proxy) { described_class.new(session_claims: session_claims, session_token: session_token) } 17 | let(:sdk_double) { instance_double(Clerk::SDK) } 18 | 19 | before do 20 | allow(Clerk::SDK).to receive(:new).and_return(sdk_double) 21 | end 22 | 23 | describe "#user?" do 24 | it "returns true when session_claims exists" do 25 | expect(proxy.user?).to be true 26 | end 27 | 28 | it "returns false when session_claims is nil" do 29 | proxy = described_class.new 30 | expect(proxy.user?).to be false 31 | end 32 | end 33 | 34 | describe "#user" do 35 | let(:user_object) { double("User") } 36 | 37 | before do 38 | allow(sdk_double).to receive_message_chain(:users, :get_user).with(user_id).and_return(user_object) 39 | end 40 | 41 | it "returns nil when not authenticated" do 42 | proxy = described_class.new 43 | expect(proxy.user).to be_nil 44 | end 45 | 46 | it "fetches and returns user when authenticated" do 47 | expect(proxy.user).to eq(user_object) 48 | end 49 | 50 | context "with caching" do 51 | let(:cache_store) { double("CacheStore") } 52 | 53 | before do 54 | allow(Clerk.configuration).to receive(:cache_store).and_return(cache_store) 55 | end 56 | 57 | it "uses cache when available" do 58 | expect(cache_store).to receive(:fetch).with( 59 | "clerk:user:#{user_id}", 60 | expires_in: described_class::CACHE_TTL 61 | ).and_return(user_object) 62 | 63 | expect(proxy.user).to eq(user_object) 64 | end 65 | end 66 | end 67 | 68 | describe "#organization?" do 69 | it "returns true when org_id exists" do 70 | expect(proxy.organization?).to be true 71 | end 72 | 73 | it "returns false when org_id is nil" do 74 | proxy = described_class.new(session_claims: {"sub" => user_id}) 75 | expect(proxy.organization?).to be false 76 | end 77 | end 78 | 79 | describe "#organization" do 80 | let(:org_object) { double("Organization") } 81 | 82 | before do 83 | allow(sdk_double).to receive_message_chain(:organizations, :get_organization).with(org_id).and_return(org_object) 84 | end 85 | 86 | it "returns nil when no organization" do 87 | proxy = described_class.new 88 | expect(proxy.organization).to be_nil 89 | end 90 | 91 | it "fetches and returns organization when present" do 92 | expect(proxy.organization).to eq(org_object) 93 | end 94 | end 95 | 96 | describe "#organization_role" do 97 | it "returns nil when no session_claims" do 98 | proxy = described_class.new 99 | expect(proxy.organization_role).to be_nil 100 | end 101 | 102 | it "fetches and returns organization role when present" do 103 | expect(proxy.organization_role).to eq(session_claims["org_role"]) 104 | end 105 | end 106 | 107 | describe "#organization_permissions" do 108 | it "returns nil when no session_claims" do 109 | proxy = described_class.new 110 | expect(proxy.organization_permissions).to be_nil 111 | end 112 | 113 | it "fetches and returns organization permissions when present" do 114 | expect(proxy.organization_permissions).to eq(session_claims["org_permissions"]) 115 | end 116 | end 117 | 118 | describe "#user_reverified?" do 119 | context "with first factor verification" do 120 | it "returns true when first factor is valid" do 121 | params = {level: :first_factor, after_minutes: 15} 122 | expect(proxy.user_reverified?(params)).to be true 123 | end 124 | 125 | it "returns false when first factor is invalid" do 126 | params = {level: :first_factor, after_minutes: 5} 127 | expect(proxy.user_reverified?(params)).to be false 128 | end 129 | end 130 | 131 | context "with second factor verification" do 132 | it "returns true when second factor is valid" do 133 | params = {level: :second_factor, after_minutes: 10} 134 | expect(proxy.user_reverified?(params)).to be true 135 | end 136 | 137 | it "returns false when second factor is invalid" do 138 | params = {level: :second_factor, after_minutes: 3} 139 | expect(proxy.user_reverified?(params)).to be false 140 | end 141 | end 142 | 143 | context "with multi factor verification" do 144 | it "returns true when both factors are valid" do 145 | params = {level: :multi_factor, after_minutes: 15} 146 | expect(proxy.user_reverified?(params)).to be true 147 | end 148 | 149 | it "returns false when either factor is invalid" do 150 | params = {level: :multi_factor, after_minutes: 7} 151 | expect(proxy.user_reverified?(params)).to be false 152 | end 153 | end 154 | end 155 | 156 | describe "#user_require_reverification!" do 157 | let(:preset) { {level: :first_factor, after_minutes: 5} } 158 | 159 | it "yields block when reverification is needed" do 160 | expect { |b| proxy.user_require_reverification!(preset, &b) }.to yield_with_args(preset) 161 | end 162 | 163 | it "doesn't yield when reverification is not needed" do 164 | allow(proxy).to receive(:user_needs_reverification?).and_return(false) 165 | expect { |b| proxy.user_require_reverification!(preset, &b) }.not_to yield_control 166 | end 167 | end 168 | 169 | describe "#user_reverification_rack_response" do 170 | it "returns a rack response array" do 171 | config = {some: "config"} 172 | response = proxy.user_reverification_rack_response(config) 173 | 174 | expect(response).to be_an(Array) 175 | expect(response[0]).to eq(403) 176 | expect(response[1]).to include(Clerk::CONTENT_TYPE_HEADER => "application/json") 177 | expect(response[2]).to be_an(Array) 178 | end 179 | 180 | it "raises ArgumentError when config is missing" do 181 | expect { proxy.user_reverification_rack_response(nil) }.to raise_error(ArgumentError) 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /spec/clerk/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "clerk/utils" 3 | 4 | RSpec.describe Clerk::Utils do 5 | describe ".filter_routes" do 6 | it "converts regular routes into a hash with true values" do 7 | routes = ["/api/v1/users", "/api/v1/posts"] 8 | filtered_routes, wildcard_routes = described_class.filter_routes(routes) 9 | 10 | expect(filtered_routes).to eq({ 11 | "/api/v1/users" => true, 12 | "/api/v1/posts" => true 13 | }) 14 | expect(wildcard_routes).to be_empty 15 | end 16 | 17 | it "handles wildcard routes correctly" do 18 | routes = ["/api/v1/*", "/admin/*"] 19 | filtered_routes, wildcard_routes = described_class.filter_routes(routes) 20 | 21 | expect(filtered_routes).to be_empty 22 | expect(wildcard_routes).to eq(["/api/v1/", "/admin/"]) 23 | end 24 | 25 | it "handles mixed regular and wildcard routes" do 26 | routes = ["/api/v1/users", "/api/v1/*", "/admin/dashboard", "/admin/*"] 27 | filtered_routes, wildcard_routes = described_class.filter_routes(routes) 28 | 29 | expect(filtered_routes).to eq({ 30 | "/api/v1/users" => true, 31 | "/admin/dashboard" => true 32 | }) 33 | expect(wildcard_routes).to eq(["/api/v1/", "/admin/"]) 34 | end 35 | 36 | it "removes duplicate wildcard routes" do 37 | routes = ["/api/v1/*", "/api/v1/*", "/admin/*"] 38 | filtered_routes, wildcard_routes = described_class.filter_routes(routes) 39 | 40 | expect(filtered_routes).to be_empty 41 | expect(wildcard_routes).to eq(["/api/v1/", "/admin/"]) 42 | end 43 | 44 | it "handles routes with whitespace" do 45 | routes = [" /api/v1/users ", " /api/v1/* "] 46 | filtered_routes, wildcard_routes = described_class.filter_routes(routes) 47 | 48 | expect(filtered_routes).to eq({ 49 | "/api/v1/users" => true 50 | }) 51 | expect(wildcard_routes).to eq(["/api/v1/"]) 52 | end 53 | end 54 | 55 | describe "#valid_publishable_key?" do 56 | it "returns true for valid live publishable key" do 57 | valid_key = "pk_live_#{Base64.encode64("test$")}" 58 | expect(described_class.valid_publishable_key?(valid_key)).to be true 59 | end 60 | 61 | it "returns true for valid test publishable key" do 62 | valid_key = "pk_test_#{Base64.encode64("test$")}" 63 | expect(described_class.valid_publishable_key?(valid_key)).to be true 64 | end 65 | 66 | it "returns false for invalid prefix" do 67 | invalid_key = "invalid_#{Base64.encode64("test$")}" 68 | expect(described_class.valid_publishable_key?(invalid_key)).to be false 69 | end 70 | 71 | it "returns false for invalid postfix" do 72 | invalid_key = "pk_test_#{Base64.encode64("test")}" 73 | expect(described_class.valid_publishable_key?(invalid_key)).to be false 74 | end 75 | 76 | it "raises ArgumentError when key cannot be converted to string" do 77 | expect { 78 | described_class.valid_publishable_key?(Object.new) 79 | }.to raise_error(ArgumentError, "publishable_key must be a string") 80 | end 81 | end 82 | 83 | describe "#valid_publishable_key_prefix?" do 84 | it "returns true for pk_live_ prefix" do 85 | expect(described_class.valid_publishable_key_prefix?("pk_live_abc")).to be true 86 | end 87 | 88 | it "returns true for pk_test_ prefix" do 89 | expect(described_class.valid_publishable_key_prefix?("pk_test_abc")).to be true 90 | end 91 | 92 | it "returns false for invalid prefix" do 93 | expect(described_class.valid_publishable_key_prefix?("invalid_abc")).to be false 94 | end 95 | end 96 | 97 | describe "#valid_publishable_key_postfix?" do 98 | it "returns true when decoded key ends with $" do 99 | key = "pk_test_#{Base64.encode64("test$")}" 100 | expect(described_class.valid_publishable_key_postfix?(key)).to be true 101 | end 102 | 103 | it "returns false when decoded key does not end with $" do 104 | key = "pk_test_#{Base64.encode64("test")}" 105 | expect(described_class.valid_publishable_key_postfix?(key)).to be false 106 | end 107 | end 108 | 109 | describe "#decode_publishable_key" do 110 | it "correctly decodes the third part of the key" do 111 | encoded = Base64.encode64("test") 112 | key = "pk_test_#{encoded}" 113 | expect(described_class.decode_publishable_key(key)).to eq "test" 114 | end 115 | 116 | it "handles invalid key format gracefully" do 117 | expect(described_class.decode_publishable_key("invalid")).to eq "" 118 | end 119 | end 120 | 121 | describe "#retrieve_from_query_string" do 122 | let(:host) { "http://example.com" } 123 | 124 | it "retrieves value from query string" do 125 | url = URI("#{host}?key=value") 126 | expect(described_class.retrieve_from_query_string(url, "key")).to eq "value" 127 | end 128 | 129 | it "returns nil when key is not present" do 130 | url = URI("#{host}?other=value") 131 | expect(described_class.retrieve_from_query_string(url, "key")).to be_nil 132 | end 133 | 134 | it "handles empty query string" do 135 | url = URI(host) 136 | expect(described_class.retrieve_from_query_string(url, "key")).to be_nil 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/clerk_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Clerk do 4 | it "has a version number" do 5 | expect(Clerk::VERSION).not_to be nil 6 | end 7 | 8 | describe ".configure" do 9 | let(:publishable_key) { "pk_test_key" } 10 | 11 | context "when a block is given" do 12 | it "yields the configuration object" do 13 | expect { |b| Clerk.configure(&b) }.to yield_with_args(Clerk::Configuration) 14 | end 15 | 16 | it "allows configuration to be set" do 17 | Clerk.configure do |config| 18 | config.publishable_key = publishable_key 19 | end 20 | 21 | expect(Clerk.configuration.publishable_key).to eq(publishable_key) 22 | end 23 | end 24 | 25 | context "when no block is given" do 26 | it "returns the configuration object" do 27 | expect(Clerk.configure).to be_an_instance_of(Clerk::Configuration) 28 | end 29 | end 30 | end 31 | 32 | describe ".configuration" do 33 | it "returns a Configuration instance" do 34 | expect(Clerk.configuration).to be_an_instance_of(Clerk::Configuration) 35 | end 36 | 37 | it "memoizes the configuration" do 38 | config = Clerk.configuration 39 | expect(Clerk.configuration).to be(config) 40 | end 41 | 42 | it "returns the default configuration initially" do 43 | allow(Clerk::Configuration).to receive(:default).and_return(double("default_config")) 44 | 45 | # Reset the memoized configuration 46 | Clerk.instance_variable_set(:@configuration, nil) 47 | 48 | expect(Clerk::Configuration).to receive(:default) 49 | Clerk.configuration 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | SimpleCov.start 5 | SimpleCov.add_filter "spec" 6 | 7 | require "clerk" 8 | require "climate_control" 9 | 10 | def with_modified_env(options = {}, &block) 11 | ClimateControl.modify(options, &block) 12 | end 13 | 14 | RSpec.configure do |config| 15 | # Enable flags like --only-failures and --next-failure 16 | config.example_status_persistence_file_path = ".rspec_status" 17 | 18 | # Disable RSpec exposing methods globally on `Module` and `main` 19 | config.disable_monkey_patching! 20 | 21 | # Allow for focused tests 22 | config.filter_run_when_matching :focus 23 | 24 | config.expect_with :rspec do |c| 25 | c.syntax = :expect 26 | end 27 | end 28 | --------------------------------------------------------------------------------