forked from abhaynikam/boring_generators
-
Notifications
You must be signed in to change notification settings - Fork 0
generator-for-devise-jwt: Implement the generator for devise-jwt #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
TheZero0-ctrl
wants to merge
1
commit into
main
Choose a base branch
from
generator-for-devise-jwt
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
189 changes: 189 additions & 0 deletions
189
lib/generators/boring/devise/jwt/install/install_generator.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'boring_generators/generator_helper' | ||
|
|
||
| module Boring | ||
| module Devise | ||
| module Jwt | ||
| class InstallGenerator < Rails::Generators::Base | ||
| include Rails::Generators::Migration | ||
| include BoringGenerators::GeneratorHelper | ||
|
|
||
| desc "Add devise-jwt to the application" | ||
| source_root File.expand_path('templates', __dir__) | ||
|
|
||
| class_option :model_name, type: :string, aliases: "-m", | ||
| default: "User", | ||
| desc: "Tell us the user model name which will be used for authentication. Defaults to User" | ||
| class_option :use_env_variable, type: :boolean, aliases: "-ev", | ||
| desc: "Use ENV variable for devise_jwt_secret_key. By default Rails credentials will be used." | ||
| class_option :revocation_strategy, type: :string, aliases: "-rs", | ||
| enum: %w[JTIMatcher Denylist Allowlist], | ||
| default: "Denylist", | ||
| desc: "Tell us the revocation strategy to be used. Defaults to Denylist" | ||
| class_option :expiration_time_in_days, type: :numeric, aliases: "-et", | ||
| default: 15, | ||
| desc: "Tell us the expiration time on days for the JWT token. Defaults to 15 days" | ||
|
|
||
| def self.next_migration_number(dirname) | ||
| next_migration_number = current_migration_number(dirname) + 1 | ||
| ActiveRecord::Migration.next_migration_number(next_migration_number) | ||
| end | ||
|
|
||
| def verify_presence_of_devise_gem | ||
| return if gem_installed?("devise") | ||
|
|
||
| say "We couldn't find devise gem. Please configure devise gem and rerun the generator. Consider running `rails generate boring:devise:install` to set up Devise.", | ||
| :red | ||
|
|
||
| abort | ||
| end | ||
|
|
||
| def verify_presence_of_devise_initializer | ||
| return if File.exist?("config/initializers/devise.rb") | ||
|
|
||
| say "We couldn't find devise initializer. Please configure devise gem and rerun the generator. Consider running `rails generate boring:devise:install` to set up Devise.", | ||
| :red | ||
|
|
||
| abort | ||
| end | ||
|
|
||
| def verify_presence_of_devise_model | ||
| return if File.exist?("app/models/#{options[:model_name].underscore}.rb") | ||
|
|
||
| say "We couldn't find the #{options[:model_name]} model. Maybe there is a typo? Please provide the correct model name and run the generator again.", :red | ||
|
|
||
| abort | ||
| end | ||
|
|
||
| def add_devise_jwt_gem | ||
| say "Adding devise-jwt gem", :green | ||
| check_and_install_gem("devise-jwt") | ||
| bundle_install | ||
| end | ||
|
|
||
| def add_devise_jwt_config_to_devise_initializer | ||
| say "Adding devise-jwt configurations to a file `config/initializers/devise.rb`", :green | ||
|
|
||
| jwt_config = <<~RUBY | ||
| config.jwt do |jwt| | ||
| jwt.secret = #{devise_jwt_secret_key} | ||
| jwt.dispatch_requests = [ | ||
| ['POST', %r{^/sign_in$}] | ||
| ] | ||
| jwt.revocation_requests = [ | ||
| ['DELETE', %r{^/sign_out$}] | ||
| ] | ||
| jwt.expiration_time = #{options[:expiration_time_in_days]}.day.to_i | ||
| end | ||
| RUBY | ||
|
|
||
| inject_into_file "config/initializers/devise.rb", | ||
| optimize_indentation(jwt_config, 2), | ||
| before: /^end\s*\Z/m | ||
|
|
||
| say "❗️❗️\nValue for jwt.secret will be used from `#{devise_jwt_secret_key}`. You can change this values if they don't match with your app.\n", | ||
| :yellow | ||
| end | ||
|
|
||
| def configure_revocation_strategies | ||
| say "Configuring #{options[:revocation_strategy]} revocation strategy", | ||
| :green | ||
|
|
||
| case options[:revocation_strategy] | ||
| when "JTIMatcher" | ||
| configure_jti_matcher_strategy | ||
| when "Denylist" | ||
| configure_denylist_strategy | ||
| when "Allowlist" | ||
| configure_allowlist_strategy | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def devise_jwt_secret_key | ||
| if options[:use_env_variable] | ||
| "ENV['DEVISE_JWT_SECRET_KEY']" | ||
| else | ||
| "Rails.application.credentials.devise_jwt_secret_key" | ||
| end | ||
| end | ||
|
|
||
| def configure_jti_matcher_strategy | ||
| @model_db_table = options[:model_name].tableize | ||
| @model_class = @model_db_table.camelcase | ||
|
|
||
| migration_template "add_jti_to_users.rb", "db/migrate/add_jti_to_#{@model_db_table}.rb" | ||
|
|
||
| add_devise_jwt_module( | ||
| strategy: "self", | ||
| include_content: "include Devise::JWT::RevocationStrategies::JTIMatcher" | ||
| ) | ||
| end | ||
|
|
||
| def configure_denylist_strategy | ||
| Bundler.with_unbundled_env do | ||
| run "bundle exec rails generate model jwt_denylist --skip-migration" | ||
| end | ||
|
|
||
| migration_template "create_jwt_denylist.rb", "db/migrate/create_jwt_denylist.rb" | ||
|
|
||
| add_devise_jwt_module(strategy: "JwtDenylist") | ||
|
|
||
| jwt_denylist_content = <<~RUBY | ||
| include Devise::JWT::RevocationStrategies::Denylist | ||
| self.table_name = 'jwt_denylist' | ||
| RUBY | ||
|
|
||
| inject_into_file "app/models/jwt_denylist.rb", | ||
| optimize_indentation(jwt_denylist_content, 2), | ||
| after: /ApplicationRecord\n/, | ||
| verbose: false | ||
| end | ||
|
|
||
| def configure_allowlist_strategy | ||
| @model_underscore = options[:model_name].underscore | ||
| Bundler.with_unbundled_env do | ||
| run "bundle exec rails generate model allowlisted_jwt --skip-migration" | ||
| end | ||
|
|
||
| migration_template "create_allowlisted_jwts.rb", "db/migrate/create_allowlisted_jwts.rb" | ||
|
|
||
| add_devise_jwt_module( | ||
| strategy: "self", | ||
| include_content: "include Devise::JWT::RevocationStrategies::Allowlist" | ||
| ) | ||
| end | ||
|
|
||
| def add_devise_jwt_module(strategy:, include_content: nil) | ||
| model_name = options[:model_name].underscore | ||
| model_content = File.read("app/models/#{model_name}.rb") | ||
| devise_module_pattern = /devise\s*(?:(?:(?::\w+)|(?:\w+:\s*\w+))(?:(?:,\s*:\w+)|(?:,\s*\w+:\s*\w+))*)/ | ||
|
|
||
| if model_content.match?(devise_module_pattern) | ||
| inject_into_file "app/models/#{model_name}.rb", | ||
| ", :jwt_authenticatable, jwt_revocation_strategy: #{strategy}", | ||
| after: devise_module_pattern | ||
| else | ||
| inject_into_file "app/models/#{model_name}.rb", | ||
| optimize_indentation( | ||
| "devise :jwt_authenticatable, jwt_revocation_strategy: #{strategy}", | ||
| 2 | ||
| ), | ||
| after: /ApplicationRecord\n/ | ||
| say "Successfully added the devise-jwt module to #{model_name} model. However, it looks like the devise module is missing from the #{model_name} model. Please configure the devise module to ensure everything functions correctly.", | ||
| :yellow | ||
| end | ||
|
|
||
| if include_content | ||
| inject_into_file "app/models/#{model_name}.rb", | ||
| optimize_indentation(include_content, 2), | ||
| after: /ApplicationRecord\n/, | ||
| verbose: false | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
6 changes: 6 additions & 0 deletions
6
lib/generators/boring/devise/jwt/install/templates/add_jti_to_users.rb.tt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| class AddJtiTo<%= @model_class %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] | ||
| def change | ||
| add_column :<%= @model_db_table %>, :jti, :string, null: false | ||
| add_index :<%= @model_db_table %>, :jti, unique: true | ||
| end | ||
| end |
15 changes: 15 additions & 0 deletions
15
lib/generators/boring/devise/jwt/install/templates/create_allowlisted_jwts.rb.tt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| class CreateAllowlistedJwts < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] | ||
| def change | ||
| create_table :allowlisted_jwts do |t| | ||
| t.string :jti, null: false | ||
| t.string :aud | ||
| # If you want to leverage the `aud` claim, add to it a `NOT NULL` constraint: | ||
| # t.string :aud, null: false | ||
| t.datetime :exp, null: false | ||
| t.references :<%= @model_underscore %>, foreign_key: { on_delete: :cascade }, null: false | ||
|
|
||
| t.timestamps | ||
| end | ||
| add_index :allowlisted_jwts, :jti | ||
| end | ||
| end |
11 changes: 11 additions & 0 deletions
11
lib/generators/boring/devise/jwt/install/templates/create_jwt_denylist.rb.tt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| class CreateJwtDenylist < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] | ||
| def change | ||
| create_table :jwt_denylist do |t| | ||
| t.string :jti, null: false | ||
| t.datetime :exp, null: false | ||
|
|
||
| t.timestamps | ||
| end | ||
| add_index :jwt_denylist, :jti | ||
| end | ||
| end |
136 changes: 136 additions & 0 deletions
136
test/generators/devise/devise_jwt_install_generator_test.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require "test_helper" | ||
| require "generators/boring/devise/jwt/install/install_generator" | ||
|
|
||
| class DeviseInstallGeneratorTest < Rails::Generators::TestCase | ||
| tests Boring::Devise::Jwt::InstallGenerator | ||
| setup :build_app | ||
| teardown :teardown_app | ||
|
|
||
| include GeneratorHelper | ||
| include ActiveSupport::Testing::Isolation | ||
|
|
||
| def destination_root | ||
| app_path | ||
| end | ||
|
|
||
| def test_should_exit_if_devise_is_not_installed | ||
| assert_raises SystemExit do | ||
| quietly { generator.verify_presence_of_devise_gem } | ||
| end | ||
| end | ||
|
|
||
| def test_should_exit_if_devise_initializer_is_not_present | ||
| assert_raises SystemExit do | ||
| quietly { generator.verify_presence_of_devise_initializer } | ||
| end | ||
| end | ||
|
|
||
| def test_should_exit_if_devise_model_is_not_present | ||
| assert_raises SystemExit do | ||
| quietly { generator.verify_presence_of_devise_model } | ||
| end | ||
| end | ||
|
|
||
| def test_should_configure_devise_jwt | ||
| Dir.chdir(app_path) do | ||
| setup_devise | ||
| quietly { run_generator } | ||
| assert_gem "devise-jwt" | ||
| assert_file "config/initializers/devise.rb" do |content| | ||
| assert_match(/config.jwt do |jwt|/, content) | ||
| assert_match(/jwt.secret = Rails.application.credentials.devise_jwt_secret/, content) | ||
| assert_match(/jwt\.dispatch_requests\s*=\s*\[\s*/, content) | ||
| assert_match(/jwt\.revocation_requests\s*=\s*\[\s*/, content) | ||
| assert_match(/jwt\.expiration_time\s*=\s*/, content) | ||
| end | ||
| assert_migration "db/migrate/create_jwt_denylist.rb" | ||
| assert_file "app/models/user.rb" do |content| | ||
| assert_match( | ||
| /devise\s*(?:(?:(?::\w+)|(?:\w+:\s*\w+))(?:(?:,\s*:\w+)|(?:,\s*\w+:\s*\w+))*)*(?:,\s*:jwt_authenticatable, jwt_revocation_strategy: JwtDenylist)/, | ||
| content | ||
| ) | ||
| end | ||
|
|
||
| assert_file "app/models/jwt_denylist.rb" do |content| | ||
| assert_match(/include Devise::JWT::RevocationStrategies::Denylist/, content) | ||
| assert_match(/self\.table_name = 'jwt_denylist'/, content) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def test_should_use_env_variable_for_devise_jwt_secret | ||
| Dir.chdir(app_path) do | ||
| setup_devise | ||
| quietly { run_generator [destination_root, "--use_env_variable"] } | ||
| assert_file "config/initializers/devise.rb" do |content| | ||
| assert_match(/jwt\.secret\s*=\s*ENV\['DEVISE_JWT_SECRET_KEY'\]/, content) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def test_should_configure_jti_matcher_revocation_strategy | ||
| Dir.chdir(app_path) do | ||
| setup_devise | ||
| quietly { run_generator [destination_root, "--revocation_strategy=JTIMatcher"] } | ||
| assert_migration "db/migrate/add_jti_to_users.rb" | ||
| assert_file "app/models/user.rb" do |content| | ||
| assert_match(/include Devise::JWT::RevocationStrategies::JTIMatcher/, content) | ||
| assert_match( | ||
| /devise\s*(?:(?:(?::\w+)|(?:\w+:\s*\w+))(?:(?:,\s*:\w+)|(?:,\s*\w+:\s*\w+))*)*(?:,\s*:jwt_authenticatable, jwt_revocation_strategy: self)/, | ||
| content | ||
| ) | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def test_should_configure_allowlist_revocation_strategy | ||
| Dir.chdir(app_path) do | ||
| setup_devise | ||
| quietly { run_generator [destination_root, "--revocation_strategy=Allowlist"] } | ||
| assert_migration "db/migrate/create_allowlisted_jwts.rb" | ||
| assert_file "app/models/user.rb" do |content| | ||
| assert_match(/include Devise::JWT::RevocationStrategies::Allowlist/, content) | ||
| assert_match( | ||
| /devise\s*(?:(?:(?::\w+)|(?:\w+:\s*\w+))(?:(?:,\s*:\w+)|(?:,\s*\w+:\s*\w+))*)*(?:,\s*:jwt_authenticatable, jwt_revocation_strategy: self)/, | ||
| content | ||
| ) | ||
| end | ||
| assert_file "app/models/allowlisted_jwt.rb" | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def setup_devise(model_name: "User") | ||
| Bundler.with_unbundled_env do | ||
| `bundle add devise` | ||
| end | ||
|
|
||
| create_devise_initializer | ||
| create_devise_model(model_name) | ||
| end | ||
|
|
||
| def create_devise_initializer | ||
| FileUtils.mkdir_p("#{app_path}/config/initializers") | ||
| content = <<~RUBY | ||
| Devise.setup do |config| | ||
| end | ||
| RUBY | ||
|
|
||
| File.write("#{app_path}/config/initializers/devise.rb", content) | ||
| end | ||
|
|
||
| def create_devise_model(model_name) | ||
| FileUtils.mkdir_p("#{app_path}/app/models") | ||
| content = <<~RUBY | ||
| class #{model_name} < ApplicationRecord | ||
| devise :database_authenticatable, :registerable, | ||
| :recoverable, :rememberable, :validatable | ||
| end | ||
| RUBY | ||
|
|
||
| File.write("#{app_path}/app/models/#{model_name.underscore}.rb", content) | ||
| end | ||
| end | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.