Skip to content

Commit 14bc3f5

Browse files
committed
production hardening
1 parent ae0c60b commit 14bc3f5

22 files changed

Lines changed: 240 additions & 31 deletions

app/controllers/admin/agenda_items_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Admin
22
class AgendaItemsController < BaseController
3+
rate_limit to: 3, within: 1.minute, only: :auto_tag_all, with: -> { redirect_to untagged_admin_agenda_items_path, alert: "Too many requests. Please wait a moment." }
4+
35
def untagged
46
scope = AgendaItem.left_joins(:agenda_item_tags)
57
.where(agenda_item_tags: { id: nil })
@@ -39,6 +41,7 @@ def auto_tag_all
3941
.count
4042
tagged_now = items.size - still_untagged
4143

44+
audit("agenda_items.auto_tag_all", metadata: { tagged: tagged_now, still_untagged: still_untagged })
4245
redirect_to untagged_admin_agenda_items_path,
4346
notice: "Auto-tagged #{tagged_now} items. #{still_untagged} still need manual tagging."
4447
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Admin
2+
class AuditLogsController < BaseController
3+
before_action :require_site_admin
4+
5+
def index
6+
@audit_logs = AdminAuditLog.includes(:user).recent
7+
8+
if params[:action_filter].present?
9+
@audit_logs = @audit_logs.where(action: params[:action_filter])
10+
end
11+
12+
if params[:user_id].present?
13+
@audit_logs = @audit_logs.where(user_id: params[:user_id])
14+
end
15+
16+
@audit_logs = @audit_logs.limit(100)
17+
@action_types = AdminAuditLog.distinct.pluck(:action).sort
18+
@admin_users = User.where(role: [ :content_admin, :site_admin ]).order(:name)
19+
end
20+
end
21+
end

app/controllers/admin/base_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Admin
22
class BaseController < ApplicationController
3+
include Auditable
4+
35
before_action :require_content_admin_or_above
46

57
layout "admin"

app/controllers/admin/blog_posts_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def create
1818
@blog_post.user = Current.user
1919

2020
if @blog_post.save
21+
audit("blog_post.create", target: @blog_post)
2122
redirect_to admin_blog_post_path(@blog_post), notice: "Blog post created."
2223
else
2324
render :new, status: :unprocessable_entity
@@ -36,16 +37,19 @@ def update
3637
end
3738

3839
def destroy
40+
audit("blog_post.destroy", target: @blog_post, metadata: { title: @blog_post.title })
3941
@blog_post.destroy!
4042
redirect_to admin_blog_posts_path, notice: "Blog post deleted."
4143
end
4244

4345
def publish
4446
if @blog_post.published?
4547
@blog_post.unpublish!
48+
audit("blog_post.unpublish", target: @blog_post)
4649
redirect_to admin_blog_post_path(@blog_post), notice: "Blog post unpublished."
4750
else
4851
@blog_post.publish!
52+
audit("blog_post.publish", target: @blog_post)
4953
redirect_to admin_blog_post_path(@blog_post), notice: "Blog post published."
5054
end
5155
end

app/controllers/admin/council_members_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def create
1313
@council_member = CouncilMember.new(council_member_params)
1414

1515
if @council_member.save
16+
audit("council_member.create", target: @council_member)
1617
redirect_to admin_council_members_path, notice: "Council member added."
1718
else
1819
render :new, status: :unprocessable_entity
@@ -27,6 +28,7 @@ def update
2728
@council_member = CouncilMember.find(params[:id])
2829

2930
if @council_member.update(council_member_params)
31+
audit("council_member.update", target: @council_member)
3032
redirect_to admin_council_members_path, notice: "Council member updated."
3133
else
3234
render :edit, status: :unprocessable_entity
@@ -35,6 +37,7 @@ def update
3537

3638
def destroy
3739
@council_member = CouncilMember.find(params[:id])
40+
audit("council_member.destroy", target: @council_member, metadata: { name: @council_member.display_name })
3841
@council_member.destroy!
3942
redirect_to admin_council_members_path, notice: "Council member deleted."
4043
end

app/controllers/admin/dashboard_controller.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ def show
6969

7070
# --- Blog ---
7171
@draft_posts = BlogPost.draft.chronological.limit(3)
72+
73+
# --- Audit log ---
74+
@recent_audit_logs = AdminAuditLog.includes(:user).recent.limit(10)
7275
end
7376
end
7477
end

app/controllers/admin/invitations_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def create
1616
@invitation.invited_by = Current.session.user
1717

1818
if @invitation.save
19+
audit("invitation.create", target: @invitation, metadata: { role: @invitation.role })
1920
redirect_to admin_invitations_path, notice: "Invitation created."
2021
else
2122
render :new, status: :unprocessable_entity
@@ -24,6 +25,7 @@ def create
2425

2526
def destroy
2627
@invitation = Invitation.find(params[:id])
28+
audit("invitation.revoke", target: @invitation)
2729
@invitation.destroy
2830
redirect_to admin_invitations_path, notice: "Invitation revoked."
2931
end

app/controllers/admin/meetings_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Admin
22
class MeetingsController < BaseController
3+
rate_limit to: 5, within: 1.minute, only: %i[create import_minutes auto_tag], with: -> { redirect_back fallback_location: admin_meetings_path, alert: "Too many requests. Please wait a moment." }
4+
35
def index
46
@meetings = Meeting.includes(agenda_versions: { agenda_sections: :agenda_items }).chronological
57
end
@@ -48,6 +50,7 @@ def create
4850
service = AgendaImportService.new(data).call
4951

5052
if service.success?
53+
audit("agenda.import", target: service.meeting, metadata: { version: service.agenda_version.version_number })
5154
notice = if service.new_version?
5255
"New version (v#{service.agenda_version.version_number}) added for this meeting."
5356
else
@@ -98,6 +101,7 @@ def apply_agenda
98101
Rails.cache.delete(cache_key)
99102

100103
if service.success?
104+
audit("agenda.apply_changes", target: @meeting, metadata: { changes_applied: accepted.size })
101105
redirect_to admin_meeting_path(@meeting), notice: "#{accepted.size} change(s) applied successfully."
102106
else
103107
redirect_to admin_meeting_path(@meeting), alert: service.errors.join(", ")
@@ -129,6 +133,7 @@ def import_minutes
129133
service = MinutesImportService.new(data).call
130134

131135
if service.success?
136+
audit("minutes.import", target: @meeting)
132137
notice = "Minutes imported successfully."
133138
notice += " Warnings: #{service.warnings.join('; ')}" if service.warnings.any?
134139
redirect_to admin_meeting_path(@meeting), notice: notice
@@ -147,6 +152,7 @@ def delete_minutes
147152
end
148153
end
149154

155+
audit("minutes.delete", target: @meeting)
150156
redirect_to admin_meeting_path(@meeting), notice: "Minutes data deleted."
151157
end
152158

@@ -155,9 +161,11 @@ def publish
155161
@agenda_version = @meeting.agenda_versions.find(params[:version_id])
156162
if @agenda_version.published?
157163
@agenda_version.unpublish!
164+
audit("agenda.unpublish", target: @meeting, metadata: { version: @agenda_version.version_number })
158165
redirect_to admin_meeting_path(@meeting), notice: "Version #{@agenda_version.version_number} unpublished."
159166
else
160167
@agenda_version.publish!
168+
audit("agenda.publish", target: @meeting, metadata: { version: @agenda_version.version_number })
161169
redirect_to admin_meeting_path(@meeting), notice: "Version #{@agenda_version.version_number} published."
162170
end
163171
end
@@ -170,12 +178,14 @@ def auto_tag
170178
AutoTaggingService.new(items).call
171179

172180
tagged_count = items.count { |i| i.tags.reload.any? }
181+
audit("meeting.auto_tag", target: @meeting, metadata: { tagged: tagged_count, total: items.size })
173182
redirect_to admin_meeting_path(@meeting),
174183
notice: "Auto-tagged #{tagged_count} of #{items.size} items."
175184
end
176185

177186
def destroy
178187
@meeting = Meeting.find(params[:id])
188+
audit("meeting.destroy", target: @meeting, metadata: { date: @meeting.date.to_s })
179189
@meeting.destroy!
180190
redirect_to admin_meetings_path, notice: "Meeting deleted."
181191
end

app/controllers/admin/tags_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
module Admin
22
class TagsController < BaseController
3+
rate_limit to: 3, within: 1.minute, only: :seed_rules, with: -> { redirect_to admin_tags_path, alert: "Too many requests. Please wait a moment." }
4+
35
SORT_COLUMNS = {
46
"name" => "LOWER(tags.name)",
57
"items" => "COUNT(agenda_item_tags.id)"
@@ -40,12 +42,14 @@ def update
4042

4143
def destroy
4244
@tag = Tag.find(params[:id])
45+
audit("tag.destroy", target: @tag, metadata: { name: @tag.name })
4346
@tag.destroy
4447
redirect_to admin_tags_path(q: params[:q]), notice: "Tag \"#{@tag.name}\" deleted."
4548
end
4649

4750
def seed_rules
4851
AutoTaggingService.seed_default_rules!
52+
audit("tags.seed_rules")
4953
redirect_to admin_tags_path, notice: "Default auto-tag rules seeded."
5054
end
5155

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Auditable
2+
extend ActiveSupport::Concern
3+
4+
private
5+
6+
def audit(action, target: nil, metadata: {})
7+
AdminAuditLog.log(action: action, target: target, metadata: metadata)
8+
end
9+
end

0 commit comments

Comments
 (0)