rest of ip address role model / admin dash work
This commit is contained in:
77
app/controllers/state/ip_address_roles_controller.rb
Normal file
77
app/controllers/state/ip_address_roles_controller.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
# typed: true
|
||||
class State::IpAddressRolesController < ApplicationController
|
||||
before_action :set_ip_address_role, only: %i[edit update destroy]
|
||||
before_action :authorize_ip_address_roles
|
||||
|
||||
# GET /state/ip_address_roles
|
||||
def index
|
||||
@ip_address_roles = IpAddressRole.all.order(created_at: :desc)
|
||||
end
|
||||
|
||||
# GET /state/ip_address_roles/new
|
||||
def new
|
||||
@ip_address_role = IpAddressRole.new
|
||||
end
|
||||
|
||||
# GET /state/ip_address_roles/1/edit
|
||||
def edit
|
||||
end
|
||||
|
||||
# POST /state/ip_address_roles
|
||||
def create
|
||||
@ip_address_role = IpAddressRole.new(ip_address_role_params)
|
||||
|
||||
if @ip_address_role.save
|
||||
redirect_to state_ip_address_roles_path,
|
||||
notice: "IP address role was successfully created."
|
||||
else
|
||||
render :new
|
||||
end
|
||||
end
|
||||
|
||||
# PATCH/PUT /state/ip_address_roles/1
|
||||
def update
|
||||
if @ip_address_role.update(ip_address_role_params)
|
||||
redirect_to state_ip_address_roles_path,
|
||||
notice: "IP address role was successfully updated."
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /state/ip_address_roles/1
|
||||
def destroy
|
||||
@ip_address_role.destroy
|
||||
redirect_to state_ip_address_roles_path,
|
||||
notice: "IP address role was successfully deleted."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Use callbacks to share common setup or constraints between actions
|
||||
def set_ip_address_role
|
||||
@ip_address_role = IpAddressRole.find(params[:id])
|
||||
end
|
||||
|
||||
# Only allow a list of trusted parameters through
|
||||
def ip_address_role_params
|
||||
params.require(:ip_address_role).permit(
|
||||
:ip_address,
|
||||
:role,
|
||||
:description,
|
||||
:active,
|
||||
)
|
||||
end
|
||||
|
||||
# Authorize all actions based on the current action
|
||||
def authorize_ip_address_roles
|
||||
case action_name.to_sym
|
||||
when :index, :new, :edit
|
||||
authorize IpAddressRole, policy_class: State::IpAddressRolePolicy
|
||||
when :create
|
||||
authorize IpAddressRole, policy_class: State::IpAddressRolePolicy
|
||||
when :update, :destroy
|
||||
authorize @ip_address_role, policy_class: State::IpAddressRolePolicy
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/helpers/ip_address_helper.rb
Normal file
35
app/helpers/ip_address_helper.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# typed: strict
|
||||
module IpAddressHelper
|
||||
extend T::Sig
|
||||
|
||||
# Formats an IPAddr object to display properly with CIDR notation if it's a subnet
|
||||
# @param ip_addr [IPAddr, nil] The IP address object to format or nil
|
||||
# @return [String] A formatted string representation of the IP address
|
||||
sig { params(ip_addr: T.nilable(IPAddr)).returns(String) }
|
||||
def format_ip_address(ip_addr)
|
||||
if ip_addr.nil?
|
||||
""
|
||||
else
|
||||
# For IPv4, check if the prefix is not 32 (full mask)
|
||||
# For IPv6, check if the prefix is not 128 (full mask)
|
||||
if (ip_addr.ipv4? && ip_addr.prefix < 32) ||
|
||||
(ip_addr.ipv6? && ip_addr.prefix < 128)
|
||||
# This is a CIDR range
|
||||
"#{ip_addr.to_s}/#{ip_addr.prefix}"
|
||||
else
|
||||
# Single IP address
|
||||
ip_addr.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Determines if the provided IP address is a CIDR range
|
||||
# @param ip_addr [IPAddr, nil] The IP address to check or nil
|
||||
# @return [Boolean] true if the address is a CIDR range, false otherwise
|
||||
sig { params(ip_addr: T.nilable(IPAddr)).returns(T::Boolean) }
|
||||
def cidr_range?(ip_addr)
|
||||
return false if ip_addr.nil?
|
||||
|
||||
format_ip_address(ip_addr).include?("/")
|
||||
end
|
||||
end
|
||||
170
app/javascript/bundles/UI/components/IpAddressInput.tsx
Normal file
170
app/javascript/bundles/UI/components/IpAddressInput.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface IpAddressInputProps {
|
||||
initialValue?: string;
|
||||
name: string;
|
||||
id?: string;
|
||||
placeholder?: string;
|
||||
onChange?: (value: string, isValid: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type ValidationResult = {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
type: 'ip' | 'cidr' | 'none';
|
||||
};
|
||||
|
||||
const IpAddressInput: React.FC<IpAddressInputProps> = ({
|
||||
initialValue = '',
|
||||
name = 'ip_address_role[ip_address]',
|
||||
id,
|
||||
placeholder = 'Example: 192.168.1.1 or 10.0.0.0/24',
|
||||
onChange,
|
||||
className = '',
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const [validation, setValidation] = useState<ValidationResult>({
|
||||
isValid: true,
|
||||
message: '',
|
||||
type: 'none',
|
||||
});
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Regular expressions for validation
|
||||
const ipv4Regex =
|
||||
/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/;
|
||||
const cidrRegex =
|
||||
/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}\/(?:[0-9]|[1-2][0-9]|3[0-2])$/;
|
||||
|
||||
// Function to validate the input
|
||||
const validateIpAddress = (input: string): ValidationResult => {
|
||||
if (!input) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'IP address is required',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
if (input.includes('/')) {
|
||||
// CIDR notation
|
||||
if (cidrRegex.test(input)) {
|
||||
return { isValid: true, message: 'Valid CIDR range', type: 'cidr' };
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid CIDR range format',
|
||||
type: 'cidr',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Single IP address
|
||||
if (ipv4Regex.test(input)) {
|
||||
return { isValid: true, message: 'Valid IP address', type: 'ip' };
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid IP address format',
|
||||
type: 'ip',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update validation when value changes
|
||||
useEffect(() => {
|
||||
const result = validateIpAddress(value);
|
||||
setValidation(result);
|
||||
|
||||
// Call onChange callback if provided
|
||||
if (onChange) {
|
||||
onChange(value, result.isValid);
|
||||
}
|
||||
}, [value, onChange]);
|
||||
|
||||
// Get border color based on validation state
|
||||
const getBorderColorClass = () => {
|
||||
if (!isFocused) return 'border-slate-300';
|
||||
if (value === '') return 'border-sky-500';
|
||||
return validation.isValid ? 'border-emerald-500' : 'border-red-500';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
className={`block w-full rounded-md shadow-sm focus:ring-sky-500 sm:text-sm ${getBorderColorClass()} ${className}`}
|
||||
id={id || 'ip_address_input'}
|
||||
/>
|
||||
|
||||
{/* This is a direct input that will be properly included in form submission */}
|
||||
<input
|
||||
type="text"
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Validation feedback */}
|
||||
{value !== '' && (
|
||||
<div
|
||||
className={`mt-1 text-sm ${validation.isValid ? 'text-emerald-600' : 'text-red-600'}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{validation.isValid ? (
|
||||
<svg
|
||||
className="mr-1 h-4 w-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="mr-1 h-4 w-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
<span>{validation.message}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional helpful information */}
|
||||
<div className="mt-2 text-sm text-slate-500">
|
||||
{validation.type === 'cidr' ? (
|
||||
<span>
|
||||
CIDR notation represents an IP range (e.g., 10.0.0.0/24 includes 256
|
||||
addresses)
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
Enter a single IP address (e.g., 192.168.1.1) or an IP range in CIDR
|
||||
notation (e.g., 10.0.0.0/24)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IpAddressInput;
|
||||
1
app/javascript/bundles/UI/components/index.ts
Normal file
1
app/javascript/bundles/UI/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as IpAddressInput } from './IpAddressInput';
|
||||
@@ -0,0 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import ReactOnRails from 'react-on-rails';
|
||||
import { IpAddressInput } from '../components';
|
||||
|
||||
// This is how react_on_rails can see the component in the browser.
|
||||
ReactOnRails.register({
|
||||
IpAddressInput,
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import { UserMenu } from '../bundles/Main/components/UserMenu';
|
||||
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
|
||||
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
|
||||
import { initCollapsibleSections } from '../bundles/UI/collapsibleSections';
|
||||
import { IpAddressInput } from '../bundles/UI/components';
|
||||
|
||||
// This is how react_on_rails can see the components in the browser.
|
||||
ReactOnRails.register({
|
||||
@@ -12,6 +13,7 @@ ReactOnRails.register({
|
||||
UserMenu,
|
||||
PostHoverPreviewWrapper,
|
||||
UserHoverPreviewWrapper,
|
||||
IpAddressInput,
|
||||
});
|
||||
|
||||
// Initialize collapsible sections
|
||||
|
||||
73
app/policies/state/ip_address_role_policy.rb
Normal file
73
app/policies/state/ip_address_role_policy.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
# typed: true
|
||||
class State::IpAddressRolePolicy < ApplicationPolicy
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def index?
|
||||
user_is_admin? || ip_is_admin?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def create?
|
||||
user_is_admin? || ip_is_admin?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def new?
|
||||
create?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def update?
|
||||
user_is_admin? || ip_is_admin?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def edit?
|
||||
update?
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def destroy?
|
||||
user_is_admin? || ip_is_admin?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def user_is_admin?
|
||||
T.cast(user, T.nilable(User))&.admin? || false
|
||||
end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def ip_is_admin?
|
||||
ip_role = @controller.current_ip_address_role
|
||||
ip_role&.admin? || false
|
||||
end
|
||||
|
||||
class Scope
|
||||
extend T::Sig
|
||||
|
||||
sig do
|
||||
params(
|
||||
user: T.nilable(User),
|
||||
scope: T.untyped,
|
||||
controller: ApplicationController,
|
||||
).void
|
||||
end
|
||||
def initialize(user, scope, controller)
|
||||
@user = user
|
||||
@scope = scope
|
||||
@controller = controller
|
||||
end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def resolve
|
||||
if @user&.admin? || @controller.current_ip_address_role&.admin?
|
||||
@scope
|
||||
else
|
||||
@scope.where(id: nil) # Returns empty relation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
81
app/views/state/ip_address_roles/_form.html.erb
Normal file
81
app/views/state/ip_address_roles/_form.html.erb
Normal file
@@ -0,0 +1,81 @@
|
||||
<%= form_with(model: [:state, ip_address_role], local: true, class: "space-y-6") do |form| %>
|
||||
<% if ip_address_role.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= pluralize(ip_address_role.errors.count, "error") %> prohibited this IP address role from being saved:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc space-y-1 pl-5">
|
||||
<% ip_address_role.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<%= form.label :ip_address, class: "block text-sm font-medium text-slate-700" %>
|
||||
<div class="mt-1">
|
||||
<%= react_component("IpAddressInput", {
|
||||
prerender: false,
|
||||
props: {
|
||||
name: "ip_address_role[ip_address]",
|
||||
id: "ip_address_role_ip_address",
|
||||
initialValue: format_ip_address(ip_address_role.ip_address),
|
||||
placeholder: "Example: 192.168.1.1 or 10.0.0.0/24"
|
||||
}
|
||||
}) %>
|
||||
</div>
|
||||
<% if ip_address_role.errors[:ip_address].any? %>
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<%= ip_address_role.errors[:ip_address].join(", ") %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :role, class: "block text-sm font-medium text-slate-700" %>
|
||||
<div class="mt-1">
|
||||
<%= form.select :role,
|
||||
IpAddressRole.roles.keys.map { |r| [r.titleize, r] },
|
||||
{},
|
||||
class: "block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-500">Select the role that will be assigned to this IP address or range.</p>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-slate-700" %>
|
||||
<div class="mt-1">
|
||||
<%= form.text_area :description,
|
||||
class: "block w-full rounded-md border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 sm:text-sm",
|
||||
rows: 3,
|
||||
placeholder: "Describe why this IP address role is being added" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-slate-500">Provide a description for future reference (e.g., "Office network", "Admin home IP").</p>
|
||||
</div>
|
||||
<div class="relative flex items-start">
|
||||
<div class="flex h-5 items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500" %>
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<%= form.label :active, class: "font-medium text-slate-700" %>
|
||||
<p class="text-slate-500">Uncheck to temporarily disable this IP address role without deleting it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-5">
|
||||
<div class="flex justify-end space-x-3">
|
||||
<%= link_to 'Cancel',
|
||||
state_ip_address_roles_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" %>
|
||||
<%= form.submit 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>
|
||||
</div>
|
||||
<% end %>
|
||||
23
app/views/state/ip_address_roles/edit.html.erb
Normal file
23
app/views/state/ip_address_roles/edit.html.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<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 IP Address Role</h1>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Modify an existing IP address role for <code class="bg-slate-100 px-1 py-0.5 rounded text-slate-800"><%= @ip_address_role.ip_address %></code>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 space-x-3 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<%= link_to 'View Details',
|
||||
state_ip_address_role_path(@ip_address_role),
|
||||
class: "bg-sky-500 hover:bg-sky-700 text-white font-bold py-2 px-4 rounded" %>
|
||||
<%= link_to 'Back to List',
|
||||
state_ip_address_roles_path,
|
||||
class: "bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= render 'form', ip_address_role: @ip_address_role %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
117
app/views/state/ip_address_roles/index.html.erb
Normal file
117
app/views/state/ip_address_roles/index.html.erb
Normal file
@@ -0,0 +1,117 @@
|
||||
<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">IP Address Roles</h1>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Manage roles assigned to IP addresses and CIDR ranges.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<%= link_to 'Add New IP Address Role',
|
||||
new_state_ip_address_role_path,
|
||||
class: "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" %>
|
||||
</div>
|
||||
</div>
|
||||
<% if @ip_address_roles.any? %>
|
||||
<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">IP Address/Range</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Role</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Description</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Status</th>
|
||||
<th class="pb-2 text-left text-sm font-semibold text-slate-900">Created</th>
|
||||
<th class="relative pb-2"><span class="sr-only">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200">
|
||||
<% @ip_address_roles.each do |ip_role| %>
|
||||
<tr>
|
||||
<td class="py-2 pr-4 text-sm font-medium text-slate-900">
|
||||
<code class="bg-slate-100 px-1 py-0.5 rounded text-slate-800"><%= format_ip_address(ip_role.ip_address) %></code>
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-sm">
|
||||
<% pill_color =
|
||||
case ip_role.role
|
||||
when "admin"
|
||||
"bg-rose-100 text-rose-700"
|
||||
when "moderator"
|
||||
"bg-amber-100 text-amber-700"
|
||||
else
|
||||
"bg-sky-100 text-sky-700"
|
||||
end
|
||||
%>
|
||||
<span class="<%= pill_color %> inline-flex items-center rounded-full px-2.5 py-0.5 font-medium">
|
||||
<%= ip_role.role.titleize %>
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-sm text-slate-500">
|
||||
<%= ip_role.description %>
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-sm">
|
||||
<% if ip_role.active? %>
|
||||
<span class="text-emerald-600 font-medium">Active</span>
|
||||
<% else %>
|
||||
<span class="text-slate-500 font-medium">Inactive</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-2 pr-4 text-sm text-slate-500">
|
||||
<%= ip_role.created_at.strftime('%Y-%m-%d %H:%M') %>
|
||||
</td>
|
||||
<td class="py-2 text-right text-sm font-medium">
|
||||
<%= link_to 'Edit',
|
||||
edit_state_ip_address_role_path(ip_role),
|
||||
class: "text-blue-600 hover:text-blue-900 mr-3" %>
|
||||
<%= button_to 'Delete',
|
||||
state_ip_address_role_path(ip_role),
|
||||
method: :delete,
|
||||
class: "text-red-600 hover:text-red-900 inline",
|
||||
form: {
|
||||
class: "inline",
|
||||
data: {
|
||||
confirm: 'Are you sure you want to delete this IP address role?'
|
||||
}
|
||||
} %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="mt-6 rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
No IP address roles have been created yet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="mt-8 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-slate-900">About IP Address Roles</h3>
|
||||
<div class="mt-2 max-w-xl text-sm text-slate-500">
|
||||
<p>IP address roles allow you to grant permissions to specific IP addresses or IP ranges without requiring a user account.</p>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h4 class="text-md font-medium text-slate-900">Usage Information</h4>
|
||||
<ul class="mt-2 list-disc pl-5 text-sm text-slate-700 space-y-1">
|
||||
<li>Use <strong>single IP addresses</strong> like <code class="bg-slate-100 px-1 py-0.5 rounded">192.168.1.1</code> to grant roles to a specific IP.</li>
|
||||
<li>Use <strong>CIDR notation</strong> like <code class="bg-slate-100 px-1 py-0.5 rounded">192.168.1.0/24</code> to grant roles to an entire subnet.</li>
|
||||
<li>Roles will be checked from most specific to least specific, with user accounts taking precedence.</li>
|
||||
<li>Overlapping IP ranges with different roles are not allowed to prevent conflicts.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
20
app/views/state/ip_address_roles/new.html.erb
Normal file
20
app/views/state/ip_address_roles/new.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<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">New IP Address Role</h1>
|
||||
<p class="mt-2 text-sm text-slate-700">
|
||||
Create a new role for an IP address or CIDR range.
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
|
||||
<%= link_to 'Back to List',
|
||||
state_ip_address_roles_path,
|
||||
class: "bg-slate-500 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= render 'form', ip_address_role: @ip_address_role %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,19 +57,24 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :blobs, controller: :blob_entries, only: [:show], param: :sha256
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
authenticate :user, ->(user) { user.admin? } do
|
||||
# IP address roles management
|
||||
namespace :state do
|
||||
resources :ip_address_roles, except: [:show]
|
||||
end
|
||||
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
mount GoodJob::Engine => "jobs"
|
||||
mount PgHero::Engine => "pghero"
|
||||
|
||||
|
||||
1
sorbet/rbi/dsl/application_controller.rbi
generated
1
sorbet/rbi/dsl/application_controller.rbi
generated
@@ -46,6 +46,7 @@ class ApplicationController
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::DomainSourceHelper
|
||||
include ::GoodJobHelper
|
||||
include ::IpAddressHelper
|
||||
include ::SourceHelper
|
||||
include ::TimestampHelper
|
||||
include ::UiHelper
|
||||
|
||||
1
sorbet/rbi/dsl/devise_controller.rbi
generated
1
sorbet/rbi/dsl/devise_controller.rbi
generated
@@ -43,6 +43,7 @@ class DeviseController
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::DomainSourceHelper
|
||||
include ::GoodJobHelper
|
||||
include ::IpAddressHelper
|
||||
include ::SourceHelper
|
||||
include ::TimestampHelper
|
||||
include ::UiHelper
|
||||
|
||||
12
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
12
sorbet/rbi/dsl/generated_path_helpers_module.rbi
generated
@@ -39,6 +39,9 @@ module GeneratedPathHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_global_state_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_state_ip_address_role_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_user_password_path(*args); end
|
||||
|
||||
@@ -99,6 +102,9 @@ module GeneratedPathHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_rails_conductor_inbound_email_source_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_state_ip_address_role_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_user_password_path(*args); end
|
||||
|
||||
@@ -198,6 +204,12 @@ module GeneratedPathHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def search_by_name_domain_users_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def state_ip_address_role_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def state_ip_address_roles_path(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def stats_log_entries_path(*args); end
|
||||
|
||||
|
||||
12
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
12
sorbet/rbi/dsl/generated_url_helpers_module.rbi
generated
@@ -39,6 +39,9 @@ module GeneratedUrlHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_global_state_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_state_ip_address_role_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def edit_user_password_url(*args); end
|
||||
|
||||
@@ -99,6 +102,9 @@ module GeneratedUrlHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_rails_conductor_inbound_email_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_state_ip_address_role_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def new_user_password_url(*args); end
|
||||
|
||||
@@ -198,6 +204,12 @@ module GeneratedUrlHelpersModule
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def search_by_name_domain_users_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def state_ip_address_role_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def state_ip_address_roles_url(*args); end
|
||||
|
||||
sig { params(args: T.untyped).returns(String) }
|
||||
def stats_log_entries_url(*args); end
|
||||
|
||||
|
||||
1
sorbet/rbi/dsl/rails/application_controller.rbi
generated
1
sorbet/rbi/dsl/rails/application_controller.rbi
generated
@@ -46,6 +46,7 @@ class Rails::ApplicationController
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::DomainSourceHelper
|
||||
include ::GoodJobHelper
|
||||
include ::IpAddressHelper
|
||||
include ::SourceHelper
|
||||
include ::TimestampHelper
|
||||
include ::UiHelper
|
||||
|
||||
@@ -46,6 +46,7 @@ class Rails::Conductor::BaseController
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::DomainSourceHelper
|
||||
include ::GoodJobHelper
|
||||
include ::IpAddressHelper
|
||||
include ::SourceHelper
|
||||
include ::TimestampHelper
|
||||
include ::UiHelper
|
||||
|
||||
1
sorbet/rbi/dsl/rails/health_controller.rbi
generated
1
sorbet/rbi/dsl/rails/health_controller.rbi
generated
@@ -46,6 +46,7 @@ class Rails::HealthController
|
||||
include ::Domain::PostGroupsHelper
|
||||
include ::DomainSourceHelper
|
||||
include ::GoodJobHelper
|
||||
include ::IpAddressHelper
|
||||
include ::SourceHelper
|
||||
include ::TimestampHelper
|
||||
include ::UiHelper
|
||||
|
||||
Reference in New Issue
Block a user