Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active April 28, 2025 11:27
Show Gist options
  • Save amkisko/c704c1a6462d573dfa4820ae07d807a6 to your computer and use it in GitHub Desktop.
Save amkisko/c704c1a6462d573dfa4820ae07d807a6 to your computer and use it in GitHub Desktop.
ActiveAdmin v4 propshaft, importmap, stimulus, tailwindcss and ActionPolicy configuration
$ rails new . -n ActiveAdminDemo -c tailwind -a propshaft --skip-test --skip-system-test

$ rails g active_admin:install --skip-users
$ rails tailwindcss:install
$ rails generate active_admin:assets
$ cat tailwind-active_admin.config.js | sed 's/require(`@activeadmin\/activeadmin\/plugin`)/require(`${activeAdminPath}\/plugin.js`)/g' > config/tailwind-active_admin.config.js
$ rm tailwind-active_admin.config.js
$ bundle binstub tailwindcss-rails
$ rails generate active_admin:views
$ echo "active_admin: bin/rails active_admin:watch" >> Procfile.dev

$ vim lib/tasks/active_admin.rake
$ vim config/initializers/active_admin.rb

$ rails g model button clicked:boolean clicked_at:timestamp
$ rails generate active_admin:resource Button

$ rails db:create db:migrate db:seed
$ bin/dev
module ActiveAdmin
class ActionPolicyAdapter < AuthorizationAdapter
def authorized?(action, subject = nil)
target = policy_target(subject)
policy = ActionPolicy.lookup(target)
action = format_action(action, subject)
policy.new(target, user: user).apply(action)
end
def scope_collection(collection, _action = Auth::READ)
target = policy_target(collection)
policy = ActionPolicy.lookup(target)
policy.new(user: user).apply_scope(collection, type: :active_admin)
end
def format_action(action, subject)
case action
when Auth::CREATE
:create?
when Auth::UPDATE
:update?
when Auth::READ
subject.is_a?(Class) ? :index? : :show?
when Auth::DESTROY
subject.is_a?(Class) ? :destroy_all? : :destroy?
else
"#{action}?"
end
end
private
def policy_target(subject)
case subject
when nil
resource.resource_class
when Class
subject.new
else
subject
end
end
end
end
namespace :active_admin do
desc "Build Active Admin Tailwind stylesheets"
task build: :environment do
command = [
Rails.root.join("bin/tailwindcss").to_s,
"-i", Rails.root.join("app/assets/stylesheets/active_admin.css").to_s,
"-o", Rails.root.join("app/assets/builds/active_admin.css").to_s,
"-c", Rails.root.join("config/tailwind-active_admin.config.js").to_s,
"-m"
]
system(*command, exception: true)
end
desc "Watch Active Admin Tailwind stylesheets"
task watch: :environment do
command = [
Rails.root.join("bin/tailwindcss").to_s,
"--watch",
"-i", Rails.root.join("app/assets/stylesheets/active_admin.css").to_s,
"-o", Rails.root.join("app/assets/builds/active_admin.css").to_s,
"-c", Rails.root.join("config/tailwind-active_admin.config.js").to_s,
"-m"
]
system(*command)
end
end
Rake::Task["assets:precompile"].enhance(["active_admin:build"])
Rake::Task["test:prepare"].enhance(["active_admin:build"]) if Rake::Task.task_defined?("test:prepare")
Rake::Task["spec:prepare"].enhance(["tailwindcss:build"]) if Rake::Task.task_defined?("spec:prepare")
Rake::Task["db:test:prepare"].enhance(["tailwindcss:build"]) if Rake::Task.task_defined?("db:test:prepare")
class Current < ActiveSupport::CurrentAttributes
attribute :user
attribute :request_id, :user_agent, :remote_addr
attribute :locale, :time_zone
resets do
I18n.locale = I18n.default_locale
Time.zone = Time.zone_default
ActionReporter.reset_context
end
def user_agent=(user_agent)
super
ActionReporter.context(user_agent: user_agent)
end
def remote_addr=(remote_addr)
super
ActionReporter.context(remote_addr: remote_addr)
end
def locale=(locale)
super
I18n.locale = locale
ActionReporter.context(locale: locale)
end
def time_zone=(time_zone)
super
Time.zone = time_zone
ActionReporter.context(time_zone: time_zone)
end
def user=(user)
super
ActionReporter.audited_user = user
end
end
require_relative "../../app/lib/active_admin/action_policy_adapter"
ActiveAdmin.importmap.draw do
pin "@rails/actioncable", to: "actioncable.esm.js", preload: true
pin "@rails/activestorage", to: "activestorage.esm.js", preload: true
pin "@hotwired/turbo-rails", to: "turbo.js", preload: true
pin "@hotwired/stimulus", to: "stimulus.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin "application", preload: true
pin_all_from "app/assets/javascripts/controllers", under: "controllers"
end
ActiveAdmin.setup do |config|
# ...
config.authorization_adapter = ActiveAdmin::ActionPolicyAdapter
# ...
end
# https://guides.rubyonrails.org/security.html
# Define an application-wide content security policy
# For further information see the following documentation
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.img_src :self, :https, :data
policy.script_src :self, :https
policy.style_src :self, :https, :unsafe_inline
# if Rails.env.production?
# policy.report_uri -> { "https://api.honeybadger.io/v1/browser/csp?api_key=#{HoneybadgerConfig.new.api_key}&report_only=true&env=#{EnvConfig.new.environment}&context[user_id]=#{respond_to?(:current_user) ? current_user&.id : nil}" }
# end
end
# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
# Set the nonce only to specific directives
Rails.application.config.content_security_policy_nonce_directives = %w[script-src]
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
# Rails.application.config.content_security_policy_report_only = true
const execSync = require('child_process').execSync;
const activeAdminPath = execSync('bundle show activeadmin', { encoding: 'utf-8' }).trim();
module.exports = {
content: [
`${activeAdminPath}/vendor/javascript/flowbite.js`,
`${activeAdminPath}/plugin.js`,
`${activeAdminPath}/app/views/**/*.{arb,erb,html,rb}`,
'./app/admin/**/*.{arb,erb,html,rb}',
'./app/views/active_admin/**/*.{arb,erb,html,rb}',
'./app/views/admin/**/*.{arb,erb,html,rb}',
'./app/javascript/**/*.js'
],
darkMode: "class",
plugins: [
require(`${activeAdminPath}\/plugin.js`)
]
}
# NOTE: partial content required for Gemfile
gem "rails"
gem "propshaft"
gem "importmap-rails"
gem "stimulus-rails"
gem "tailwindcss-rails"
gem "action_policy"
gem "activeadmin", "4.0.0.beta6"
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "active_admin" %>
<% # On page load or when changing themes, best to add inline in `head` to avoid FOUC %>
<%= javascript_tag nonce: true do %>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
<% end %>
<%= javascript_importmap_tags "active_admin", importmap: ActiveAdmin.importmap %>
<%= javascript_import_module_tag "application" %>
<%= render partial: "honeybadger", locals: { layout: :active_admin } %>
<%= render partial: "favicon" %>
<%= render partial: "fonts" %>
<%= render partial: "scripts" %>
<% if content_for?(:head) %>
<%= yield(:head) %>
<% end %>
<%= javascript_include_tag "https://js.honeybadger.io/v6.8/honeybadger.min.js" nonce: true %>
<% if ENV["HONEYBADGER_API_KEY"].present? %>
<%= javascript_tag nonce: true do %>
if (typeof Honeybadger !== "undefined") {
Honeybadger.configure({
apiKey: "<%= ENV.fetch("HONEYBADGER_API_KEY") %>",
environment: "<%= ENV.fetch("HONEYBADGER_ENV") %>",
revision: "<%= ENV.fetch("HONEYBADGER_REVISION", "unknown") %>",
});
Honeybadger.setContext({
layout: "<%= layout %>",
user_id: "<%= current_user&.id %>"
});
}
<% end %>
<% end %>
<%= javascript_include_tag "https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js", nonce: true, defer: true %>
@amkisko
Copy link
Author

amkisko commented Apr 28, 2025

@hannesfostie Hey! I think it is related to extensibility of ActiveAdmin, you can generate view templates and modify default behavior to anything you would like to achieve. Think of ActiveAdmin as an extension or set of ready-made models-views-controllers that you can modify any time. E.g. here: https://github.com/activeadmin/activeadmin/blob/master/app/views/active_admin/shared/_resource_comments.html.erb#L5 -- I just checked one of production projects and we have authorization implemented there.

@hannesfostie
Copy link

hannesfostie commented Apr 28, 2025

@amkisko maybe I phrased my question poorly - what I meant was I couldn't find context around what this gist is. Based on your reply it sounds like it's not necessarily going to be a part of ActiveAdmin, and that this is rather a custom adapter/implementation you created?

FWIW: there could be value in submitting a PR for this adapter, for official support! I have a hunch ActionPolicy will gain ground over CanCan and Pundit

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment