Add Inkbunny credentials management functionality

- Introduced methods for managing Inkbunny cookies in the GlobalStatesController, including `ib_cookies`, `edit_ib_cookies`, and `update_ib_cookies`.
- Added a new policy for managing Inkbunny cookies, restricting access to admin users.
- Created views for displaying and editing Inkbunny credentials, enhancing user interaction.
- Updated routes to include paths for Inkbunny cookies management.
- Enhanced tests for the new functionality in the GlobalStatesController spec, ensuring proper handling of credentials.
This commit is contained in:
Dylan Knutson
2024-12-30 03:15:08 +00:00
parent c9858ee354
commit 44778f6541
9 changed files with 507 additions and 505 deletions

View File

@@ -8,6 +8,8 @@ class GlobalStatesController < ApplicationController
furaffinity-cookie-oaid
].freeze
IB_COOKIE_KEYS = %w[inkbunny-username inkbunny-password inkbunny-sid].freeze
def index
authorize GlobalState
@global_states = policy_scope(GlobalState).order(:key)
@@ -94,6 +96,56 @@ class GlobalStatesController < ApplicationController
end
end
def ib_cookies
authorize GlobalState
@ib_cookies =
IB_COOKIE_KEYS.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
end
def edit_ib_cookies
authorize GlobalState
@ib_cookies =
IB_COOKIE_KEYS
.reject { |key| key == "inkbunny-sid" }
.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
@ib_sid = GlobalState.find_by(key: "inkbunny-sid")
end
def update_ib_cookies
authorize GlobalState
begin
ActiveRecord::Base.transaction do
ib_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 ib_cookies_global_states_path,
notice: "Inkbunny credentials were successfully updated."
rescue ActiveRecord::RecordInvalid => e
@ib_cookies =
IB_COOKIE_KEYS
.reject { |key| key == "inkbunny-sid" }
.map do |key|
GlobalState.find_by(key: key) ||
GlobalState.new(key: key, value_type: :string)
end
@ib_sid = GlobalState.find_by(key: "inkbunny-sid")
flash.now[:alert] = "Error updating Inkbunny credentials: #{e.message}"
render :edit_ib_cookies, status: :unprocessable_entity
end
end
private
def set_global_state
@@ -107,4 +159,10 @@ class GlobalStatesController < ApplicationController
def fa_cookies_params
params.require(:fa_cookies).permit(*FA_COOKIE_KEYS)
end
def ib_cookies_params
params.require(:ib_cookies).permit(
*IB_COOKIE_KEYS.reject { |key| key == "inkbunny-sid" },
)
end
end

View File

@@ -19,14 +19,6 @@ class GlobalStatePolicy < ApplicationPolicy
user.admin?
end
def new?
create?
end
def edit?
update?
end
def fa_cookies?
user.admin?
end
@@ -39,9 +31,21 @@ class GlobalStatePolicy < ApplicationPolicy
user.admin?
end
def ib_cookies?
user.admin?
end
def edit_ib_cookies?
user.admin?
end
def update_ib_cookies?
user.admin?
end
class Scope < Scope
def resolve
user.admin? ? scope.all : scope.none
scope.all
end
end
end

View File

@@ -0,0 +1,73 @@
<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 Inkbunny Credentials
</h1>
<p class="mt-2 text-sm text-slate-700">
Update the credentials used for Inkbunny authentication.
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
<%= link_to "Back to Credentials",
ib_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 ib_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">
Field
</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">
<% @ib_cookies.each do |cookie| %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
<%= cookie.key.sub("inkbunny-", "").titleize %>
</td>
<td class="py-2 pr-4">
<%= text_field_tag "ib_cookies[#{cookie.key}]",
cookie.value,
type: cookie.key == "inkbunny-password" ? "password" : "text",
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 %>
<% if @ib_sid %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
Session ID
</td>
<td class="py-2 pr-4 text-sm text-slate-500">
<%= @ib_sid.value.present? ? @ib_sid.value : "(not set)" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="mt-4 flex justify-end space-x-3">
<%= link_to "Cancel",
ib_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,55 @@
<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">
Inkbunny Credentials
</h1>
<p class="mt-2 text-sm text-slate-700">
Manage credentials for Inkbunny 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 Credentials",
ib_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">
Field
</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">
<% @ib_cookies.each do |cookie| %>
<tr>
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
<%= cookie.key.sub("inkbunny-", "").titleize %>
</td>
<td class="py-2 pr-4 text-sm text-slate-500">
<% if cookie.key == "inkbunny-password" && cookie.value.present? %>
••••••••
<% else %>
<%= cookie.value.present? ? cookie.value : "(not set)" %>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -11,6 +11,10 @@
fa_cookies_global_states_path,
class:
"bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
<%= link_to "Manage IB Cookies",
ib_cookies_global_states_path,
class:
"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:

View File

@@ -94,6 +94,10 @@ Rails.application.routes.draw 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"
get "ib-cookies", to: "global_states#ib_cookies"
get "ib-cookies/edit", to: "global_states#edit_ib_cookies"
patch "ib-cookies", to: "global_states#update_ib_cookies"
end
end
end

613
db/schema.rb generated

File diff suppressed because it is too large Load Diff

View File

@@ -250,4 +250,149 @@ RSpec.describe GlobalStatesController, type: :controller do
end
end
end
describe "Inkbunny Credentials Management" do
let(:ib_cookie_keys) do
%w[inkbunny-username inkbunny-password inkbunny-sid]
end
describe "GET #ib_cookies" do
context "when no Inkbunny credentials exist" do
before { GlobalState.delete_all }
it "returns a success response" do
get :ib_cookies
expect(response).to be_successful
end
it "assigns new GlobalState objects for all required credentials" do
get :ib_cookies
assigned_credentials = assigns(:ib_cookies)
expect(assigned_credentials.size).to eq(3)
expect(assigned_credentials).to all(be_new_record)
expect(assigned_credentials.map(&:key)).to match_array(ib_cookie_keys)
expect(assigned_credentials.map(&:value_type).uniq).to eq(["string"])
end
end
context "when Inkbunny credentials exist" do
it "assigns all Inkbunny credentials" do
username = create(:global_state, :inkbunny_username)
password = create(:global_state, :inkbunny_password)
sid = create(:global_state, :inkbunny_sid)
get :ib_cookies
expect(assigns(:ib_cookies)).to match_array([username, password, sid])
end
end
end
describe "GET #edit_ib_cookies" do
context "when no Inkbunny credentials exist" do
before { GlobalState.delete_all }
it "returns a success response" do
get :edit_ib_cookies
expect(response).to be_successful
end
it "assigns new GlobalState objects for editable credentials" do
get :edit_ib_cookies
assigned_credentials = assigns(:ib_cookies)
expect(assigned_credentials.size).to eq(2)
expect(assigned_credentials).to all(be_new_record)
expect(assigned_credentials.map(&:key)).to match_array(
%w[inkbunny-username inkbunny-password],
)
expect(assigned_credentials.map(&:value_type).uniq).to eq(["string"])
end
it "has a valid route" do
expect(ib_cookies_edit_global_states_path).to eq(
"/state/ib-cookies/edit",
)
end
end
context "when Inkbunny credentials exist" do
it "assigns editable credentials and session ID" do
username = create(:global_state, :inkbunny_username)
password = create(:global_state, :inkbunny_password)
sid = create(:global_state, :inkbunny_sid)
get :edit_ib_cookies
expect(assigns(:ib_cookies)).to match_array([username, password])
expect(assigns(:ib_sid)).to eq(sid)
end
end
end
describe "PATCH #update_ib_cookies" do
context "with valid parameters" do
let(:valid_params) do
{
ib_cookies: {
"inkbunny-username" => "newuser",
"inkbunny-password" => "newpass",
},
}
end
it "creates or updates the credentials" do
patch :update_ib_cookies, params: valid_params
username = GlobalState.find_by(key: "inkbunny-username")
password = GlobalState.find_by(key: "inkbunny-password")
expect(username.value).to eq("newuser")
expect(username.value_type).to eq("string")
expect(password.value).to eq("newpass")
expect(password.value_type).to eq("string")
end
it "redirects to the credentials page" do
patch :update_ib_cookies, params: valid_params
expect(response).to redirect_to(ib_cookies_global_states_path)
end
it "sets a success notice" do
patch :update_ib_cookies, params: valid_params
expect(flash[:notice]).to eq(
"Inkbunny credentials were successfully updated.",
)
end
end
context "with invalid parameters" do
let(:invalid_params) do
{
ib_cookies: {
"inkbunny-username" => "",
"inkbunny-password" => "",
},
}
end
it "does not create new credentials" do
expect {
patch :update_ib_cookies, params: invalid_params
}.not_to change(GlobalState, :count)
end
it "renders the edit template" do
patch :update_ib_cookies, params: invalid_params
expect(response).to render_template(:edit_ib_cookies)
end
it "sets an error alert" do
patch :update_ib_cookies, params: invalid_params
expect(flash.now[:alert]).to match(
/Error updating Inkbunny credentials:/,
)
end
end
end
end
end

View File

@@ -1,7 +1,43 @@
FactoryBot.define do
factory :global_state do
sequence(:key) { |n| "key_#{n}" }
sequence(:key) { |n| "test_key_#{n}" }
value { "test_value" }
value_type { :string }
trait :inkbunny_username do
key { "inkbunny-username" }
value { "testuser" }
value_type { :string }
end
trait :inkbunny_password do
key { "inkbunny-password" }
value { "testpass" }
value_type { :string }
end
trait :inkbunny_sid do
key { "inkbunny-sid" }
value { "testsid" }
value_type { :string }
end
trait :counter do
sequence(:key) { |n| "counter_#{n}" }
value { "123" }
value_type { :counter }
end
trait :duration do
sequence(:key) { |n| "duration_#{n}" }
value { "30s" }
value_type { :duration }
end
trait :password do
sequence(:key) { |n| "password_#{n}" }
value { "secret" }
value_type { :password }
end
end
end