Add FA Cookies management functionality

- Introduced methods for managing FurAffinity cookies in the GlobalStatesController, including `fa_cookies`, `edit_fa_cookies`, and `update_fa_cookies`.
- Added a new policy for managing FA cookies, restricting access to admin users.
- Created views for displaying and editing FA cookies, enhancing user interaction.
- Updated routes to include paths for FA cookies management.
- Added comprehensive tests for the new functionality in the GlobalStatesController spec.
This commit is contained in:
Dylan Knutson
2024-12-30 01:39:21 +00:00
parent fbc3a53c25
commit 28ab0cc023
7 changed files with 406 additions and 99 deletions

View File

@@ -2,6 +2,12 @@ class GlobalStatesController < ApplicationController
before_action :set_global_state, only: %i[edit update destroy]
after_action :verify_authorized
FA_COOKIE_KEYS = %w[
furaffinity-cookie-a
furaffinity-cookie-b
furaffinity-cookie-oaid
].freeze
def index
authorize GlobalState
@global_states = policy_scope(GlobalState).order(:key)
@@ -44,6 +50,50 @@ class GlobalStatesController < ApplicationController
notice: "Global state was successfully deleted."
end
def fa_cookies
authorize GlobalState
@fa_cookies =
FA_COOKIE_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
end
def edit_fa_cookies
authorize GlobalState
@fa_cookies =
FA_COOKIE_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
end
def update_fa_cookies
authorize GlobalState
begin
ActiveRecord::Base.transaction do
fa_cookies_params.each do |key, value|
state = GlobalState.find_or_initialize_by(key: key)
state.value = value
state.value_type = :string
state.save!
end
end
redirect_to fa_cookies_global_states_path,
notice: "FA cookies were successfully updated."
rescue ActiveRecord::RecordInvalid => e
@fa_cookies =
FA_COOKIE_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
flash.now[:alert] = "Error updating FA cookies: #{e.message}"
render :edit_fa_cookies, status: :unprocessable_entity
end
end
private
def set_global_state
@@ -53,4 +103,8 @@ class GlobalStatesController < ApplicationController
def global_state_params
params.require(:global_state).permit(:key, :value, :value_type)
end
def fa_cookies_params
params.require(:fa_cookies).permit(*FA_COOKIE_KEYS)
end
end

View File

@@ -27,6 +27,18 @@ class GlobalStatePolicy < ApplicationPolicy
update?
end
def fa_cookies?
user.admin?
end
def edit_fa_cookies?
user.admin?
end
def update_fa_cookies?
user.admin?
end
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none

View File

@@ -0,0 +1,62 @@
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-slate-900">
Edit FurAffinity Cookies
</h1>
<p class="mt-2 text-sm text-slate-700">
Update the cookie values used for FurAffinity authentication.
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<%= link_to "Back to Cookies",
fa_cookies_global_states_path,
class:
"bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
</div>
</div>
<div class="mt-6 overflow-hidden bg-white shadow sm:rounded-lg">
<div class="p-3 sm:p-4">
<%= form_tag fa_cookies_global_states_path, method: :patch do %>
<table class="min-w-full divide-y divide-slate-300">
<thead>
<tr>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Cookie
</th>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Value
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<% @fa_cookies.each do |cookie| %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
<%= cookie.key.sub("furaffinity-cookie-", "").upcase %>
</td>
<td class="py-2 pr-4">
<%= text_field_tag "fa_cookies[#{cookie.key}]",
cookie.value,
class:
"block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="mt-4 flex justify-end space-x-3">
<%= link_to "Cancel",
fa_cookies_global_states_path,
class:
"rounded-md border border-slate-300 bg-white py-2 px-4 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2" %>
<%= submit_tag "Save",
class:
"inline-flex justify-center rounded-md border border-transparent bg-sky-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2" %>
</div>
<% end %>
</div>
</div>
</div>

View File

@@ -0,0 +1,49 @@
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="sm:flex sm:items-center">
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-slate-900">FurAffinity Cookies</h1>
<p class="mt-2 text-sm text-slate-700">
Manage cookies for FurAffinity authentication.
</p>
</div>
<div class="mt-4 space-x-4 sm:ml-16 sm:mt-0 sm:flex-none">
<%= link_to "Back to Global States",
global_states_path,
class:
"bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded mr-3" %>
<%= link_to "Edit Cookies",
fa_cookies_edit_global_states_path,
class:
"bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
</div>
</div>
<div class="mt-6 overflow-hidden bg-white shadow sm:rounded-lg">
<div class="p-3 sm:p-4">
<table class="min-w-full divide-y divide-slate-300">
<thead>
<tr>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Cookie
</th>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Value
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<% @fa_cookies.each do |cookie| %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
<%= cookie.key.sub("furaffinity-cookie-", "").upcase %>
</td>
<td class="py-2 pr-4 text-sm text-slate-500">
<%= cookie.value.present? ? cookie.value : "(not set)" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -3,111 +3,88 @@
<div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-slate-900">Global States</h1>
<p class="mt-2 text-sm text-slate-700">
A list of all global states in the application.
A list of all global states in the system.
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<%= link_to new_global_state_path,
<div class="mt-4 space-x-4 sm:ml-16 sm:mt-0 sm:flex-none">
<%= link_to "Manage FA Cookies",
fa_cookies_global_states_path,
class:
"inline-flex items-center justify-center rounded-md border border-transparent bg-sky-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-700 focus:outline-none focus:ring-2 focus:ring-sky-500 focus:ring-offset-2 sm:w-auto" do %>
<i class="fas fa-plus mr-2"></i>
New Global State
<% end %>
"bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
<%= link_to "New Global State",
new_global_state_path,
class:
"bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
</div>
</div>
<div class="mt-8 flex flex-col">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div
class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"
>
<table class="min-w-full divide-y divide-slate-300">
<thead class="bg-slate-50">
<tr>
<th
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-slate-900 sm:pl-6"
<div class="mt-6 overflow-hidden bg-white shadow sm:rounded-lg">
<div class="p-3 sm:p-4">
<table class="min-w-full divide-y divide-slate-300">
<thead>
<tr>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Key
</th>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Value Type
</th>
<th class="pb-2 text-left text-sm font-semibold text-slate-900">
Value
</th>
<th class="relative pb-2"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<% @global_states.each do |global_state| %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
<%= global_state.key %>
</td>
<td class="py-2 pr-4 text-sm">
<% pill_color =
case global_state.value_type
when "string"
"bg-sky-100 text-sky-700"
when "counter"
"bg-emerald-100 text-emerald-700"
when "duration"
"bg-purple-100 text-purple-700"
when "password"
"bg-rose-100 text-rose-700"
end %>
<span
class="<%= pill_color %> inline-flex items-center rounded-full px-2.5 py-0.5 font-medium"
>
Key
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-slate-900"
>
Type
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-slate-900"
>
Value
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-slate-900"
>
Last Updated
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 bg-white">
<% @global_states.each do |global_state| %>
<tr>
<td
class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-slate-900 sm:pl-6"
>
<%= global_state.key %>
</td>
<td
class="whitespace-nowrap px-3 py-4 text-sm text-slate-500"
>
<span
class="<%= if global_state.value_type_password?
"bg-red-100 text-red-800"
else
"bg-sky-100 text-sky-800"
end %> inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium"
>
<%= global_state.value_type.titleize %>
</span>
</td>
<td
class="whitespace-nowrap px-3 py-4 text-sm text-slate-500"
>
<%= global_state.display_value %>
</td>
<td
class="whitespace-nowrap px-3 py-4 text-sm text-slate-500"
>
<%= time_ago_in_words(global_state.updated_at) %> ago
</td>
<td
class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6"
>
<%= link_to "Edit",
edit_global_state_path(global_state),
class: "text-sky-600 hover:text-sky-900 mr-3" %>
<%= button_to "Delete",
global_state_path(global_state),
method: :delete,
class: "text-red-600 hover:text-red-900 inline",
form: {
class: "inline",
data: {
turbo_confirm: "Are you sure?",
},
} %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<%= global_state.value_type.humanize %>
</span>
</td>
<td class="py-2 pr-4 text-sm text-slate-500">
<%= global_state.display_value %>
</td>
<td class="py-2 text-right text-sm font-medium">
<% if policy(global_state).edit? %>
<%= link_to "Edit",
edit_global_state_path(global_state),
class: "text-blue-600 hover:text-blue-900 mr-3" %>
<% end %>
<% if policy(global_state).destroy? %>
<%= button_to "Delete",
global_state_path(global_state),
method: :delete,
class: "text-red-600 hover:text-red-900 inline",
form: {
class: "inline",
data: {
turbo_confirm: "Are you sure?",
},
} %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -89,5 +89,11 @@ Rails.application.routes.draw do
end
end
resources :global_states, path: "state"
resources :global_states, path: "state" do
collection do
get "fa-cookies", to: "global_states#fa_cookies"
get "fa-cookies/edit", to: "global_states#edit_fa_cookies"
patch "fa-cookies", to: "global_states#update_fa_cookies"
end
end
end

View File

@@ -103,4 +103,151 @@ RSpec.describe GlobalStatesController, type: :controller do
expect(response).to redirect_to(global_states_path)
end
end
describe "FA Cookies Management" do
let(:fa_cookie_keys) do
%w[furaffinity-cookie-a furaffinity-cookie-b furaffinity-cookie-oaid]
end
describe "GET #fa_cookies" do
context "when no FA cookie states exist" do
before { GlobalState.delete_all }
it "returns a success response" do
get :fa_cookies
expect(response).to be_successful
end
it "assigns new GlobalState objects for all required cookies" do
get :fa_cookies
assigned_cookies = assigns(:fa_cookies)
expect(assigned_cookies.size).to eq(3)
expect(assigned_cookies).to all(be_new_record)
expect(assigned_cookies.map(&:key)).to match_array(fa_cookie_keys)
expect(assigned_cookies.map(&:value_type).uniq).to eq(["string"])
end
end
context "when FA cookie states exist" do
it "assigns all FA cookie states" do
cookie_states =
fa_cookie_keys.map do |key|
create(
:global_state,
key: key,
value: "test",
value_type: :string,
)
end
get :fa_cookies
expect(assigns(:fa_cookies)).to match_array(cookie_states)
end
end
end
describe "GET #edit_fa_cookies" do
context "when no FA cookie states exist" do
before { GlobalState.delete_all }
it "returns a success response" do
get :edit_fa_cookies
expect(response).to be_successful
end
it "assigns new GlobalState objects for all required cookies" do
get :edit_fa_cookies
assigned_cookies = assigns(:fa_cookies)
expect(assigned_cookies.size).to eq(3)
expect(assigned_cookies).to all(be_new_record)
expect(assigned_cookies.map(&:key)).to match_array(fa_cookie_keys)
expect(assigned_cookies.map(&:value_type).uniq).to eq(["string"])
end
end
context "when FA cookie states exist" do
it "assigns all FA cookie states" do
cookie_states =
fa_cookie_keys.map do |key|
create(
:global_state,
key: key,
value: "test",
value_type: :string,
)
end
get :edit_fa_cookies
expect(assigns(:fa_cookies)).to match_array(cookie_states)
end
end
end
describe "PATCH #update_fa_cookies" do
context "when no FA cookie states exist" do
before { GlobalState.delete_all }
let(:new_values) { fa_cookie_keys.index_with { |_key| "new_value" } }
it "creates all cookie states" do
expect {
patch :update_fa_cookies, params: { fa_cookies: new_values }
}.to change(GlobalState, :count).by(3)
fa_cookie_keys.each do |key|
state = GlobalState.find_by(key: key)
expect(state).to be_present
expect(state.value).to eq("new_value")
expect(state.value_type).to eq("string")
end
end
it "redirects to the fa_cookies page" do
patch :update_fa_cookies, params: { fa_cookies: new_values }
expect(response).to redirect_to(fa_cookies_global_states_path)
end
end
context "when FA cookie states exist" do
let(:new_values) { fa_cookie_keys.index_with { |_key| "new_value" } }
it "updates existing records" do
existing_states =
fa_cookie_keys.map do |key|
create(
:global_state,
key: key,
value: "old_value",
value_type: :string,
)
end
patch :update_fa_cookies, params: { fa_cookies: new_values }
existing_states.each do |state|
expect(state.reload.value).to eq("new_value")
end
end
end
context "with invalid params" do
let(:invalid_values) { { "furaffinity-cookie-a" => "" } }
it "returns unprocessable entity status" do
patch :update_fa_cookies, params: { fa_cookies: invalid_values }
expect(response).to have_http_status(:unprocessable_entity)
end
it "assigns @fa_cookies with all required keys" do
patch :update_fa_cookies, params: { fa_cookies: invalid_values }
assigned_cookies = assigns(:fa_cookies)
expect(assigned_cookies).to be_present
expect(assigned_cookies.map(&:key)).to match_array(fa_cookie_keys)
end
end
end
end
end