Skip to content

Commit 971a501

Browse files
kcdragonclaude
andauthored
Add Avo super-admin dashboard for managing users (#87)
Install Avo (Community edition) mounted at /avo, gated so only super_admins can enter. Add a User resource whose `super_admin` boolean is editable, letting a super admin promote another user. Auth is resolved by resuming the session from the signed cookie (extracted into Session.find_by_cookie, shared with the Authentication concern); non-super-admins are redirected to root. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dc01b8c commit 971a501

9 files changed

Lines changed: 279 additions & 1 deletion

File tree

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ gem "jbuilder"
2121
# HTML-aware ERB rendering via the Herb engine [https://reactionview.dev]
2222
gem "reactionview"
2323

24+
# Admin dashboard for super admins [https://avohq.io]
25+
gem "avo", "~> 3.2"
26+
2427
# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
2528
gem "bcrypt", "~> 3.1.7"
2629

Gemfile.lock

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,29 @@ GEM
104104
specs:
105105
action_text-trix (2.1.19)
106106
railties
107+
active_link_to (1.0.5)
108+
actionpack
109+
addressable
107110
addressable (2.9.0)
108111
public_suffix (>= 2.0.2, < 8.0)
109112
ast (2.4.3)
113+
avo (3.32.1)
114+
actionview (>= 6.1)
115+
active_link_to
116+
activerecord (>= 6.1)
117+
activesupport (>= 6.1)
118+
addressable
119+
avo-icons (>= 0.1.2)
120+
docile
121+
meta-tags
122+
pagy (>= 7.0.0, < 43)
123+
prop_initializer (>= 0.3.0)
124+
turbo-rails (>= 2.0.0)
125+
turbo_power (>= 0.6.0)
126+
view_component (>= 3.7.0)
127+
zeitwerk (>= 2.6.12)
128+
avo-icons (0.1.6)
129+
inline_svg
110130
base64 (0.3.0)
111131
bcrypt (3.1.22)
112132
bcrypt_pbkdf (1.1.2)
@@ -176,6 +196,9 @@ GEM
176196
actionpack (>= 6.0.0)
177197
activesupport (>= 6.0.0)
178198
railties (>= 6.0.0)
199+
inline_svg (1.10.0)
200+
activesupport (>= 3.0)
201+
nokogiri (>= 1.6)
179202
io-console (0.8.2)
180203
irb (1.18.0)
181204
pp (>= 0.6.0)
@@ -217,6 +240,8 @@ GEM
217240
net-smtp
218241
marcel (1.2.1)
219242
matrix (0.4.3)
243+
meta-tags (2.23.0)
244+
actionpack (>= 6.0.0)
220245
mini_magick (5.3.1)
221246
logger
222247
mini_mime (1.1.5)
@@ -254,6 +279,7 @@ GEM
254279
nokogiri (1.19.4-x86_64-linux-musl)
255280
racc (~> 1.4)
256281
ostruct (0.6.3)
282+
pagy (9.4.0)
257283
parallel (2.1.0)
258284
parser (3.3.11.1)
259285
ast (~> 2.4.1)
@@ -267,6 +293,8 @@ GEM
267293
prettyprint
268294
prettyprint (0.2.0)
269295
prism (1.9.0)
296+
prop_initializer (0.4.0)
297+
zeitwerk (>= 2.6.18)
270298
propshaft (1.3.2)
271299
actionpack (>= 7.0.0)
272300
activesupport (>= 7.0.0)
@@ -405,13 +433,19 @@ GEM
405433
turbo-rails (2.0.23)
406434
actionpack (>= 7.1.0)
407435
railties (>= 7.1.0)
436+
turbo_power (0.7.0)
437+
turbo-rails (>= 1.3.0)
408438
tzinfo (2.0.6)
409439
concurrent-ruby (~> 1.0)
410440
unicode-display_width (3.2.0)
411441
unicode-emoji (~> 4.1)
412442
unicode-emoji (4.2.0)
413443
uri (1.1.1)
414444
useragent (0.16.11)
445+
view_component (4.12.0)
446+
actionview (>= 7.1.0)
447+
activesupport (>= 7.1.0)
448+
concurrent-ruby (~> 1)
415449
web-console (4.3.0)
416450
actionview (>= 8.0.0)
417451
bindex (>= 0.4.0)
@@ -437,6 +471,7 @@ PLATFORMS
437471
x86_64-linux-musl
438472

439473
DEPENDENCIES
474+
avo (~> 3.2)
440475
bcrypt (~> 3.1.7)
441476
bootsnap
442477
brakeman
@@ -477,13 +512,16 @@ CHECKSUMS
477512
actionpack (8.1.3)
478513
actiontext (8.1.3)
479514
actionview (8.1.3)
515+
active_link_to (1.0.5) sha256=4830847b3d14589df1e9fc62038ceec015257fce975ec1c2a77836c461b139ba
480516
activejob (8.1.3)
481517
activemodel (8.1.3)
482518
activerecord (8.1.3)
483519
activestorage (8.1.3)
484520
activesupport (8.1.3)
485521
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
486522
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
523+
avo (3.32.1) sha256=0cbc80204734585769fdc68c2721c749b754cd3f8139d4da0667b4c4b22fe849
524+
avo-icons (0.1.6) sha256=6f1c985aca238d81446e517cee4924752e7ada1604971f6bb59913472e123f4c
487525
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
488526
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
489527
bcrypt_pbkdf (1.1.2) sha256=c2414c23ce66869b3eb9f643d6a3374d8322dfb5078125c82792304c10b94cf6
@@ -528,6 +566,7 @@ CHECKSUMS
528566
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
529567
image_processing (1.14.0) sha256=754cc169c9c262980889bec6bfd325ed1dafad34f85242b5a07b60af004742fb
530568
importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a
569+
inline_svg (1.10.0) sha256=5b652934236fd9f8adc61f3fd6e208b7ca3282698b19f28659971da84bf9a10f
531570
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
532571
irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
533572
jbuilder (2.15.1) sha256=2430bec28fb0cebacb5875b1009cf9d8bc3c303ccb810c4c8b062a4b51457637
@@ -542,6 +581,7 @@ CHECKSUMS
542581
mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941
543582
marcel (1.2.1) sha256=1678e9360e32f9eafa917c80029e2f6d10b2715c66a4b87b6d0da9b9cd1f859f
544583
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
584+
meta-tags (2.23.0) sha256=ffe78b5bee398de4ff5ac3316f5a786049538a651643b8476def06c3acc762c1
545585
mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4
546586
mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef
547587
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
@@ -562,13 +602,15 @@ CHECKSUMS
562602
nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a
563603
nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b
564604
ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912
605+
pagy (9.4.0) sha256=db3f2e043f684155f18f78be62a81e8d033e39b9f97b1e1a8d12ad38d7bce738
565606
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
566607
parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
567608
postmark (1.25.1) sha256=2ac110fa1b4f85a62996fdc63167922eea1c4284a9a7ceb9e007f2758d9e17be
568609
postmark-rails (0.22.1) sha256=fcea27dda4f7418123d7723622ec5447df4dacacc10a20a40add0f6789135d8f
569610
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
570611
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
571612
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
613+
prop_initializer (0.4.0) sha256=487d31d44a58ec7654a7db9eb23cad30030a5bbf16e6a53c71e3cbb88be9e1d8
572614
propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e
573615
psych (5.4.0) sha256=14f72d69a611af663d7d70e4a7b67d9eb1f3ae9f8d916b478961d5a0075ba5b7
574616
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
@@ -631,11 +673,13 @@ CHECKSUMS
631673
timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
632674
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
633675
turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f
676+
turbo_power (0.7.0) sha256=ad95d147e0fa761d0023ad9ca00528c7b7ddf6bba8ca2e23755d5b21b290d967
634677
tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
635678
unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
636679
unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
637680
uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
638681
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
682+
view_component (4.12.0) sha256=b9a5979d1c43eba2aa21a06a86f661aab3990104855f2909ef7c3779acdcdcb4
639683
web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4
640684
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
641685
websocket-driver (0.8.1) sha256=5ab238238ce230e5d4b262d2be39624c867914eab99171dc4952b58b577c2d96

app/avo/resources/user.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class Avo::Resources::User < Avo::BaseResource
2+
self.title = :email_address
3+
4+
def fields
5+
field :id, as: :id
6+
field :email_address, as: :text, readonly: true
7+
field :name, as: :text, readonly: true
8+
9+
field :super_admin, as: :boolean
10+
11+
field :confirmed_at, as: :date_time, readonly: true
12+
field :created_at, as: :date_time, readonly: true, only_on: :show
13+
field :updated_at, as: :date_time, readonly: true, only_on: :show
14+
15+
field :organizations, as: :has_many, through: :organization_memberships
16+
end
17+
end
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# This controller has been generated to enable Rails' resource routes.
2+
# More information on https://docs.avohq.io/3.0/controllers.html
3+
class Avo::UsersController < Avo::ResourcesController
4+
end

app/controllers/concerns/authentication.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def resume_session
2626
end
2727

2828
def find_session_by_cookie
29-
Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
29+
Session.find_by_cookie(cookies)
3030
end
3131

3232
def request_authentication

app/models/session.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
class Session < ApplicationRecord
22
belongs_to :user
3+
4+
def self.find_by_cookie(cookies)
5+
find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
6+
end
37
end

config/initializers/avo.rb

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# For more information regarding these settings check out our docs https://docs.avohq.io
2+
# The values disaplayed here are the default ones. Uncomment and change them to fit your needs.
3+
Avo.configure do |config|
4+
## == Routing ==
5+
config.root_path = "/avo"
6+
# used only when you have custom `map` configuration in your config.ru
7+
# config.prefix_path = "/internal"
8+
9+
# Where should the user be redirected when visiting the `/avo` url
10+
# config.home_path = nil
11+
12+
## == Licensing ==
13+
# config.license_key = ENV['AVO_LICENSE_KEY']
14+
15+
## == Set the context ==
16+
config.set_context do
17+
# Return a context object that gets evaluated within Avo::ApplicationController
18+
end
19+
20+
## == Authentication ==
21+
# This app has no `current_user` helper — sessions are resolved into Current.user.
22+
config.current_user_method do
23+
Current.user
24+
end
25+
26+
# Avo's controllers don't run the app's Authentication/SetCurrentOrganization
27+
# concerns, so Current is empty here. Resume the session from the signed cookie
28+
# (same lookup as Authentication#resume_session), then allow only super_admins.
29+
# Everyone else is sent back to the app's home page via main_app.
30+
config.authenticate_with do
31+
Current.session ||= Session.find_by_cookie(cookies)
32+
33+
redirect_to main_app.root_path unless Current.user&.super_admin?
34+
end
35+
36+
## == Authorization ==
37+
# config.is_admin_method = :is_admin
38+
# config.is_developer_method = :is_developer
39+
# config.authorization_methods = {
40+
# index: 'index?',
41+
# show: 'show?',
42+
# edit: 'edit?',
43+
# new: 'new?',
44+
# update: 'update?',
45+
# create: 'create?',
46+
# destroy: 'destroy?',
47+
# search: 'search?',
48+
# }
49+
# config.raise_error_on_missing_policy = false
50+
config.authorization_client = nil
51+
config.explicit_authorization = true
52+
53+
## == Localization ==
54+
# config.locale = 'en-US'
55+
56+
## == Resource options ==
57+
# config.resource_row_controls_config = {
58+
# placement: :right,
59+
# float: false,
60+
# show_on_hover: false
61+
# }.freeze
62+
# config.model_resource_mapping = {}
63+
# config.default_view_type = :table
64+
# config.per_page = 24
65+
# config.per_page_steps = [12, 24, 48, 72]
66+
# config.via_per_page = 8
67+
# config.id_links_to_resource = false
68+
# config.pagination = -> do
69+
# {
70+
# type: :default,
71+
# size: 9, # `[1, 2, 2, 1]` for pagy < 9.0
72+
# }
73+
# end
74+
75+
## == Response messages dismiss time ==
76+
# config.alert_dismiss_time = 5000
77+
78+
79+
## == Number of search results to display ==
80+
# config.search_results_count = 8
81+
82+
## == Associations lookup list limit ==
83+
# config.associations_lookup_list_limit = 1000
84+
85+
## == Cache options ==
86+
## Provide a lambda to customize the cache store used by Avo.
87+
## We compute the cache store by default, this is NOT the default, just an example.
88+
# config.cache_store = -> {
89+
# ActiveSupport::Cache.lookup_store(:solid_cache_store)
90+
# }
91+
# config.cache_resources_on_index_view = true
92+
93+
## == Turbo options ==
94+
# config.turbo = -> do
95+
# {
96+
# instant_click: true
97+
# }
98+
# end
99+
100+
## == Logger ==
101+
# config.logger = -> {
102+
# file_logger = ActiveSupport::Logger.new(Rails.root.join("log", "avo.log"))
103+
#
104+
# file_logger.datetime_format = "%Y-%m-%d %H:%M:%S"
105+
# file_logger.formatter = proc do |severity, time, progname, msg|
106+
# "[Avo] #{time}: #{msg}\n".tap do |i|
107+
# puts i
108+
# end
109+
# end
110+
#
111+
# file_logger
112+
# }
113+
114+
## == Customization ==
115+
config.click_row_to_view_record = true
116+
# config.app_name = 'Avocadelicious'
117+
# config.timezone = 'UTC'
118+
# config.currency = 'USD'
119+
# config.hide_layout_when_printing = false
120+
# config.full_width_container = false
121+
# config.full_width_index_view = false
122+
# config.search_debounce = 300
123+
# config.view_component_path = "app/components"
124+
# config.display_license_request_timeout_error = true
125+
# config.disabled_features = []
126+
# config.buttons_on_form_footers = true
127+
# config.field_wrapper_layout = true
128+
# config.resource_parent_controller = "Avo::ResourcesController"
129+
# config.first_sorting_option = :desc # :desc or :asc
130+
# config.exclude_from_status = []
131+
# config.model_generator_hook = true
132+
133+
## == Branding ==
134+
# config.branding = {
135+
# colors: {
136+
# background: "248 246 242",
137+
# 100 => "#CEE7F8",
138+
# 400 => "#399EE5",
139+
# 500 => "#0886DE",
140+
# 600 => "#066BB2",
141+
# },
142+
# chart_colors: ["#0B8AE2", "#34C683", "#2AB1EE", "#34C6A8"],
143+
# logo: "/avo-assets/logo.png",
144+
# logomark: "/avo-assets/logomark.png",
145+
# placeholder: "/avo-assets/placeholder.svg",
146+
# favicon: "/avo-assets/favicon.ico"
147+
# }
148+
149+
## == Breadcrumbs ==
150+
# config.display_breadcrumbs = true
151+
# config.set_initial_breadcrumbs do
152+
# add_breadcrumb "Home", '/avo'
153+
# end
154+
155+
## == Menus ==
156+
# config.main_menu = -> {
157+
# section "Dashboards", icon: "avo/dashboards" do
158+
# all_dashboards
159+
# end
160+
161+
# section "Resources", icon: "avo/resources" do
162+
# all_resources
163+
# end
164+
165+
# section "Tools", icon: "avo/tools" do
166+
# all_tools
167+
# end
168+
# }
169+
# config.profile_menu = -> {
170+
# link "Profile", path: "/avo/profile", icon: "heroicons/outline/user-circle"
171+
# }
172+
end

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
Rails.application.routes.draw do
2+
mount_avo
3+
24
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
35
resource :home
46
resource :dashboard, only: :show

0 commit comments

Comments
 (0)