-
-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #99 from TruemarkDev/generator-for-doorkeeper-with…
…-devise Implement the generator for doorkeeper with devise
- Loading branch information
Showing
4 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 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
190 changes: 190 additions & 0 deletions
190
lib/generators/boring/devise/doorkeeper/install/install_generator.rb
This file contains 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,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
149
test/generators/devise/doorkeeper_devise_install_generator_test.rb
This file contains 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,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 | ||
|