Skip to content

Commit

Permalink
Merge pull request #99 from TruemarkDev/generator-for-doorkeeper-with…
Browse files Browse the repository at this point in the history
…-devise

Implement the generator for doorkeeper with devise
  • Loading branch information
abhaynikam authored May 23, 2024
2 parents 5eb5dd2 + 13ed376 commit c806c84
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Adds Rack Mini Profiler generator. ([@mausamp][])
* Adds VCR generator. ([@TheZero0-ctrl][])
* Adds Pronto Generator with Gihub Action. ([@TheZero0-ctrl][])
* Adds Doorkeeper Generator with Devise. ([@TheZero0-ctrl][])

## 0.13.0 (March 26th, 2024)
* Adds Letter Opener generator. ([@coolprobn][])
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ The boring generator introduces following generators:
- Install Rack Mini Profiler: `rails generate boring:rack_mini_profiler:install`
- Install VCR: `rails generate boring:vcr:install --testing_framework=<testing_framework> --stubbing_libraries=<stubbing_libraries>`
- Install Pronto with Github Action: `rails generate boring:pronto:github_action:install`
- Install Doorkeeper with devise: `rails generate boring:devise:doorkeeper:install`

## Screencasts

Expand Down
190 changes: 190 additions & 0 deletions lib/generators/boring/devise/doorkeeper/install/install_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# frozen_string_literal: true

require 'boring_generators/generator_helper'

module Boring
module Devise
module Doorkeeper
class InstallGenerator < Rails::Generators::Base
include BoringGenerators::GeneratorHelper

desc "Adds doorkeeper with devise to the application"

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 :grant_flows, type: :array, aliases: "-gf", default: %w[authorization_code client_credentials],
enum: %w[authorization_code client_credentials password],
desc: "Tell us the grant flows you want to use separated by space. Defaults to authorization_code and client_credentials"
class_option :api_only, type: :boolean, aliases: "-a", default: false,
desc: "Tell us if you want to setup doorkeeper for API only application. Defaults to false"
class_option :skip_applications_routes, type: :boolean, aliases: "-sr", default: false,
desc: "Tell us if you want to skip adding doorkeeper routes to manage applications. Defaults to false"
class_option :use_refresh_token, type: :boolean, aliases: "-rt", default: false,
desc: "Keep user logged in with refresh tokens. Defaults to false"

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_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_doorkeeper_gem
say "Adding doorkeeper gem", :green
check_and_install_gem("doorkeeper")
bundle_install
end

def run_doorkeeper_generators
say "Running doorkeeper generators", :green

Bundler.with_unbundled_env do
run "bundle exec rails generate doorkeeper:install"
run "bundle exec rails generate doorkeeper:migration"
end
end

def add_doorkeeper_related_association_to_model
model_name = options[:model_name].underscore
say "Adding doorkeeper related associations to the model file app/models/#{model_name}.rb",
:green
model_content = <<~RUBY
has_many :access_grants,
class_name: 'Doorkeeper::AccessGrant',
foreign_key: :resource_owner_id,
dependent: :delete_all # or :destroy if you need callbacks
has_many :access_tokens,
class_name: 'Doorkeeper::AccessToken',
foreign_key: :resource_owner_id,
dependent: :delete_all # or :destroy if you need callbacks
RUBY

inject_into_file "app/models/#{model_name}.rb",
optimize_indentation(model_content, 2),
after: "ApplicationRecord\n"
end

def update_doorkeeper_initializer
say "Updating doorkeeper initializer", :green

configure_resource_owner_authenticator if options[:grant_flows].include?("authorization_code")
configure_admin_authenticator unless options[:api_only] || options[:skip_applications_routes]
configure_resource_owner_from_credentials if options[:grant_flows].include?("password")

gsub_file "config/initializers/doorkeeper.rb",
/# grant_flows %w\[authorization_code client_credentials\]/,
"grant_flows %w[#{options[:grant_flows].uniq.join(' ')}]"

if options[:api_only]
gsub_file "config/initializers/doorkeeper.rb",
/# api_only/,
"api_only"
end

if options[:skip_applications_routes]
doorkeeper_routes_content = <<~RUBY
use_doorkeeper do
skip_controllers :applications, :authorized_applications
end
RUBY

gsub_file "config/routes.rb",
/.*use_doorkeeper/,
optimize_indentation(doorkeeper_routes_content, 2)
end

if options[:use_refresh_token]
uncomment_lines "config/initializers/doorkeeper.rb",
/use_refresh_token/
end
end

def update_doorkeeper_migration
say "Updating doorkeeper migration", :green
model_name = options[:model_name].underscore

uncomment_lines Dir["db/migrate/*_create_doorkeeper_tables.rb"].first,
/add_foreign_key :oauth/

gsub_file Dir["db/migrate/*_create_doorkeeper_tables.rb"].first,
/<model>/,
":#{model_name.pluralize}"

return unless (%w[password client_credentials] & options[:grant_flows]).any?

gsub_file Dir["db/migrate/*_create_doorkeeper_tables.rb"].first,
/t.text :redirect_uri, null: false/,
"t.text :redirect_uri"
end

def show_message
return if options[:api_only] || options[:skip_applications_routes]

model_name = options[:model_name].underscore
admin_authenticator_content = "current_#{model_name} || warden.authenticate!(scope: :#{model_name})"

say "\nWe've implemented `#{admin_authenticator_content}` in the admin_authenticator block of config/initializers/doorkeeper.rb to manage access to application routes. Please adjust it as necessary to suit your requirements.",
:yellow
end

private

def configure_resource_owner_authenticator
model_name = options[:model_name].underscore
resource_owner_authenticator_content = <<~RUBY
resource_owner_authenticator do
current_#{model_name} || warden.authenticate!(scope: :#{model_name})
end
RUBY

gsub_file "config/initializers/doorkeeper.rb",
/.*resource_owner_authenticator do\n(?:\s|.)*?end/,
optimize_indentation(resource_owner_authenticator_content, 2)
end

def configure_admin_authenticator
model_name = options[:model_name].underscore
gsub_file "config/initializers/doorkeeper.rb",
/(?:# admin_authenticator do\n*)((?:\s|.)*?)(?:# end)/,
"admin_authenticator do\n" + "\\1" + "end"

admin_authenticator_content = "current_#{model_name} || warden.authenticate!(scope: :#{model_name})"
inject_into_file "config/initializers/doorkeeper.rb",
optimize_indentation(admin_authenticator_content, 4),
after: /admin_authenticator do\n/,
force: true

end

def configure_resource_owner_from_credentials
model_name = options[:model_name].underscore
resource_owner_for_credentials_content = <<~RUBY
resource_owner_from_credentials do |routes|
#{model_name} = #{options[:model_name]}.find_for_database_authentication(email: params[:email])
if #{model_name}&.valid_for_authentication? { #{model_name}.valid_password?(params[:password]) } && #{model_name}&.active_for_authentication?
request.env['warden'].set_user(#{model_name}, scope: :#{model_name}, store: false)
#{model_name}
end
end
RUBY

inject_into_file "config/initializers/doorkeeper.rb",
optimize_indentation(resource_owner_for_credentials_content, 2),
after: /resource_owner_authenticator do\n(?:\s|.)*?end\n/
end
end
end
end
end
149 changes: 149 additions & 0 deletions test/generators/devise/doorkeeper_devise_install_generator_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# frozen_string_literal: true

require "test_helper"
require "generators/boring/devise/doorkeeper/install/install_generator"

class DoorkeeperDeviseInstallGeneratorTest < Rails::Generators::TestCase
tests Boring::Devise::Doorkeeper::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_model_is_not_found
assert_raises SystemExit do
quietly { generator.verify_presence_of_devise_model }
end
end

def test_should_setup_doorkeeper
Dir.chdir(app_path) do
setup_devise
quietly { run_generator }

assert_gem "doorkeeper"
assert_file "config/locales/doorkeeper.en.yml"

assert_file "config/initializers/doorkeeper.rb" do |content|
assert_match(/resource_owner_authenticator do/, content)
assert_match(/admin_authenticator do/, content)
assert_match(/current_user || warden.authenticate!(scope: :user)/, content)
assert_no_match(/# admin_authenticator do/, content)
assert_match(/grant_flows %w\[authorization_code client_credentials\]/, content)
end

assert_migration "db/migrate/create_doorkeeper_tables.rb" do |content|
assert_no_match(/# add_foreign_key :oauth/, content)
assert_no_match(/<model>/, content)
assert_match(/add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id/, content)
assert_match(/add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id/, content)
end

assert_file "config/routes.rb" do |content|
assert_match(/use_doorkeeper/, content)
end

assert_file "app/models/user.rb" do |content|
assert_match(/has_many :access_grants/, content)
assert_match(/has_many :access_tokens/, content)
end
end
end

def test_should_setup_password_grant_flow
Dir.chdir(app_path) do
setup_devise
quietly { run_generator %w[--grant_flows=password] }

assert_migration "db/migrate/create_doorkeeper_tables.rb" do |content|
assert_match(/t.text :redirect_uri/, content)
end

assert_file "config/initializers/doorkeeper.rb" do |content|
assert_match(/grant_flows %w\[password\]/, content)
assert_match(/resource_owner_from_credentials do |routes|/, content)
assert_match(/"Please configure doorkeeper resource_owner_authenticator block/, content)
end
end
end

def test_should_setup_api_only
Dir.chdir(app_path) do
setup_devise
quietly { run_generator %w[--api_only] }

assert_file "config/initializers/doorkeeper.rb" do |content|
assert_match(/api_only/, content)
assert_no_match(/# api_only/, content)
assert_match(/# admin_authenticator do/, content)
end
end
end

def test_should_skip_applications_routes
Dir.chdir(app_path) do
setup_devise
quietly { run_generator %w[--skip_applications_routes] }

assert_file "config/routes.rb" do |content|
assert_match(/use_doorkeeper do/, content)
assert_match(/skip_controllers :applications, :authorized_applications/, content)
end
end
end

def test_should_use_refresh_token
Dir.chdir(app_path) do
setup_devise
quietly { run_generator %w[--use_refresh_token] }

assert_file "config/initializers/doorkeeper.rb" do |content|
assert_match(/use_refresh_token/, content)
assert_no_match(/# use_refresh_token/, content)
end
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
end
RUBY

File.write("#{app_path}/app/models/#{model_name.underscore}.rb", content)
end
end

0 comments on commit c806c84

Please sign in to comment.