Building secure B2B pricing grids with custom Filesystem API endpoints and role overrides
Leveraging WordPress’s Filesystem API for Secure B2B Pricing Grids
For businesses operating on a Business-to-Business (B2B) model, dynamic and secure pricing is paramount. Unlike B2C, B2B pricing often involves tiered structures, volume discounts, and customer-specific rates. WordPress, while primarily a content management system, offers robust APIs that can be extended to handle such complex requirements. This post details how to build a secure B2B pricing grid system by creating custom Filesystem API endpoints and implementing granular role-based access control (RBAC) with role overrides.
Designing the Data Structure for Pricing Grids
A flexible pricing grid requires a structured approach to data storage. We’ll opt for storing pricing data in JSON files, managed via custom endpoints. This approach offers several advantages:
- Decoupling: Separates pricing logic from the WordPress database, simplifying updates and backups.
- Performance: JSON files are generally faster to read for structured data than complex database queries, especially when cached.
- Version Control: JSON files can be easily managed with Git for versioning and auditing.
- Portability: Easy to migrate or integrate with external systems.
Each B2B customer group or tier will have its own JSON file. The file structure might look like this, representing pricing for different product SKUs and quantities:
{
"customer_tier": "premium",
"currency": "USD",
"products": {
"SKU001": {
"base_price": 100.00,
"tiers": [
{"min_quantity": 1, "price_per_unit": 100.00},
{"min_quantity": 10, "price_per_unit": 95.00},
{"min_quantity": 50, "price_per_unit": 90.00}
]
},
"SKU002": {
"base_price": 250.00,
"tiers": [
{"min_quantity": 1, "price_per_unit": 250.00},
{"min_quantity": 5, "price_per_unit": 240.00}
]
}
}
}
Implementing Custom Filesystem API Endpoints
WordPress’s REST API provides a powerful framework for creating custom endpoints. We’ll leverage this to expose functionality for reading and potentially writing (with strict controls) pricing data. The core idea is to map specific API routes to functions that interact with the filesystem.
First, we need to register our custom REST API routes. This is typically done within a plugin’s main file or an included file. We’ll use the `register_rest_route` function.
<?php
/**
* Plugin Name: B2B Pricing Grid
* Description: Custom endpoints for B2B pricing grids.
* Version: 1.0
* Author: Antigravity
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
// Define the base directory for pricing data.
define( 'B2B_PRICING_DIR', trailingslashit( WP_CONTENT_DIR ) . 'b2b-pricing-data/' );
// Ensure the directory exists.
if ( ! file_exists( B2B_PRICING_DIR ) ) {
wp_mkdir_p( B2B_PRICING_DIR );
}
/**
* Register REST API routes for pricing data.
*/
function b2b_pricing_register_routes() {
register_rest_route( 'b2b-pricing/v1', '/grid/(?P<customer_id>[\w-]+)', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'b2b_pricing_get_grid',
'permission_callback' => 'b2b_pricing_permissions_check',
'args' => array(
'customer_id' => array(
'required' => true,
'validate_callback' => function( $param, $request, $key ) {
// Basic validation: ensure it's a safe string.
return is_string( $param ) && preg_match( '/^[\w-]+$/', $param );
},
),
),
) );
// Example: Endpoint for updating pricing (highly restricted).
register_rest_route( 'b2b-pricing/v1', '/grid/(?P<customer_id>[\w-]+)', array(
'methods' => WP_REST_Server::EDITABLE, // POST, PUT, PATCH, DELETE
'callback' => 'b2b_pricing_update_grid',
'permission_callback' => 'b2b_pricing_admin_permissions_check',
'args' => array(
'customer_id' => array(
'required' => true,
'validate_callback' => function( $param, $request, $key ) {
return is_string( $param ) && preg_match( '/^[\w-]+$/', $param );
},
),
'data' => array(
'required' => true,
'validate_callback' => function( $param, $request, $key ) {
// Ensure the data is valid JSON.
json_decode( $param );
return ( json_last_error() === JSON_ERROR_NONE );
},
),
),
) );
}
add_action( 'rest_api_init', 'b2b_pricing_register_routes' );
/**
* Callback to retrieve the pricing grid for a specific customer ID.
*/
function b2b_pricing_get_grid( WP_REST_Request $request ) {
$customer_id = $request['customer_id'];
$file_path = B2B_PRICING_DIR . sanitize_file_name( $customer_id ) . '.json';
if ( ! file_exists( $file_path ) ) {
return new WP_Error( 'pricing_grid_not_found', 'Pricing grid not found for this customer.', array( 'status' => 404 ) );
}
$data = file_get_contents( $file_path );
if ( $data === false ) {
return new WP_Error( 'file_read_error', 'Could not read pricing grid file.', array( 'status' => 500 ) );
}
$pricing_data = json_decode( $data, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
return new WP_Error( 'json_decode_error', 'Invalid JSON format in pricing grid file.', array( 'status' => 500 ) );
}
// Implement caching here for performance.
// Example: Using WordPress Transients API.
$cache_key = 'b2b_pricing_grid_' . $customer_id;
set_transient( $cache_key, $pricing_data, HOUR_IN_SECONDS ); // Cache for 1 hour.
return new WP_REST_Response( $pricing_data, 200 );
}
/**
* Callback to update the pricing grid (requires admin privileges).
*/
function b2b_pricing_update_grid( WP_REST_Request $request ) {
$customer_id = $request['customer_id'];
$data_json = $request['data'];
$file_path = B2B_PRICING_DIR . sanitize_file_name( $customer_id ) . '.json';
// Data is already validated as JSON in the 'args' section.
$pricing_data = json_decode( $data_json, true );
// Further validation of the pricing structure can be added here.
$result = file_put_contents( $file_path, json_encode( $pricing_data, JSON_PRETTY_PRINT ) );
if ( $result === false ) {
return new WP_Error( 'file_write_error', 'Could not write to pricing grid file.', array( 'status' => 500 ) );
}
// Clear cache after update.
delete_transient( 'b2b_pricing_grid_' . $customer_id );
return new WP_REST_Response( array( 'message' => 'Pricing grid updated successfully.' ), 200 );
}
/**
* Permission callback for reading pricing grids.
* Checks if the user is logged in and has a role that can view pricing.
*/
function b2b_pricing_permissions_check( WP_REST_Request $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'You must be logged in to access pricing data.', array( 'status' => 401 ) );
}
// Get the current user.
$user = wp_get_current_user();
// Define roles that are allowed to view pricing.
$allowed_roles = array( 'customer', 'wholesale_customer', 'premium_customer', 'administrator' ); // Example roles.
// Check if the user has any of the allowed roles.
$can_view = false;
foreach ( $user->roles as $role ) {
if ( in_array( $role, $allowed_roles ) ) {
$can_view = true;
break;
}
}
if ( ! $can_view ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to view pricing data.', array( 'status' => 403 ) );
}
// Further checks can be implemented here, e.g., linking customer ID to user ID.
// For simplicity, we assume the customer_id in the URL is implicitly accessible by the logged-in user.
// A more robust system would map user IDs to specific customer_ids or tiers.
return true; // Permission granted.
}
/**
* Permission callback for updating pricing grids.
* Only administrators should be able to update pricing.
*/
function b2b_pricing_admin_permissions_check( WP_REST_Request $request ) {
if ( ! current_user_can( 'manage_options' ) ) { // 'manage_options' is typically assigned to Administrators.
return new WP_Error( 'rest_forbidden', 'You do not have permission to update pricing data.', array( 'status' => 403 ) );
}
return true; // Permission granted.
}
// Add a custom role for B2B customers if it doesn't exist.
function b2b_pricing_add_custom_roles() {
if ( ! get_role( 'wholesale_customer' ) ) {
add_role(
'wholesale_customer',
__( 'Wholesale Customer', 'b2b-pricing-grid' ),
array(
'read' => true, // Basic read access
'edit_posts' => false, // Cannot edit posts
'upload_files' => false, // Cannot upload files
)
);
}
if ( ! get_role( 'premium_customer' ) ) {
add_role(
'premium_customer',
__( 'Premium Customer', 'b2b-pricing-grid' ),
array(
'read' => true,
'edit_posts' => false,
'upload_files' => false,
)
);
}
}
register_activation_hook( __FILE__, 'b2b_pricing_add_custom_roles' );
// Remove custom roles on plugin deactivation.
function b2b_pricing_remove_custom_roles() {
remove_role( 'wholesale_customer' );
remove_role( 'premium_customer' );
}
register_deactivation_hook( __FILE__, 'b2b_pricing_remove_custom_roles' );
?>
In this code:
- We define a constant `B2B_PRICING_DIR` pointing to a dedicated directory outside of the theme or plugin for storing JSON pricing files. This is crucial for security and maintainability.
- `register_rest_route` sets up two endpoints: one for reading (`WP_REST_Server::READABLE`) and one for editing (`WP_REST_Server::EDITABLE`).
- The `permission_callback` is key to security. `b2b_pricing_permissions_check` ensures only logged-in users with specific roles can access pricing data. `b2b_pricing_admin_permissions_check` restricts write access to administrators.
- `b2b_pricing_get_grid` reads the JSON file, decodes it, and returns it. Basic caching using `set_transient` is included for performance.
- `b2b_pricing_update_grid` handles writing data back to the file. It’s protected by the admin-only permission callback.
- Custom roles like `wholesale_customer` and `premium_customer` are added upon plugin activation to facilitate granular access control.
Implementing Role Overrides for Granular Access
The `permission_callback` in the REST API is powerful, but sometimes you need more fine-grained control than just assigning roles. For instance, a single user might belong to multiple customer tiers or have specific overrides. WordPress’s user meta and role management can be extended for this.
Let’s say we want to associate a specific `customer_id` from our pricing grid directly with a WordPress user. This is a more secure and direct way to grant access than relying solely on generic roles.
Associating Users with Customer IDs
We can use user meta to store the `customer_id` for each user. This requires adding functionality to the user profile page in the WordPress admin.
// Add custom field to user profile page
function b2b_pricing_show_extra_profile_fields( $user ) {
?>
<?php _e( "B2B Pricing Information", "b2b-pricing-grid" ); ?>
<table class="form-table">
<tr>
<th><label for="b2b_customer_id"><?php _e( "B2B Customer ID", "b2b-pricing-grid" ); ?></label></th>
<td>
<input type="text" name="b2b_customer_id" id="b2b_customer_id" value="<?php echo esc_attr( get_user_meta( $user->ID, 'b2b_customer_id', true ) ); ?>" class="regular-text" />
<span class="description"><?php _e( "Enter the customer ID for pricing grid access.", "b2b-pricing-grid" ); ?></span>
</td>
</tr>
</table>
<?php
}
add_action( 'show_user_profile', 'b2b_pricing_show_extra_profile_fields' );
add_action( 'edit_user_profile', 'b2b_pricing_show_extra_profile_fields' );
// Save custom field data
function b2b_pricing_save_extra_profile_fields( $user_id ) {
if ( ! current_user_can( 'edit_user', $user_id ) ) {
return false;
}
if ( isset( $_POST['b2b_customer_id'] ) ) {
$customer_id = sanitize_text_field( $_POST['b2b_customer_id'] );
update_user_meta( $user_id, 'b2b_customer_id', $customer_id );
}
}
add_action( 'personal_options_update', 'b2b_pricing_save_extra_profile_fields' );
add_action( 'edit_user_profile_update', 'b2b_pricing_save_extra_profile_fields' );
?>
With this in place, administrators can assign a specific `customer_id` to each user. Now, we can modify our `b2b_pricing_permissions_check` to use this user meta.
// Modified permission callback for reading pricing grids
function b2b_pricing_permissions_check( WP_REST_Request $request ) {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'You must be logged in to access pricing data.', array( 'status' => 401 ) );
}
$user_id = get_current_user_id();
$requested_customer_id = $request['customer_id'];
// Get the customer ID associated with the logged-in user.
$user_customer_id = get_user_meta( $user_id, 'b2b_customer_id', true );
// Check if the user has a customer ID assigned.
if ( empty( $user_customer_id ) ) {
return new WP_Error( 'rest_forbidden', 'Your user account is not configured for B2B pricing.', array( 'status' => 403 ) );
}
// Crucial check: Does the requested customer ID match the user's assigned customer ID?
if ( $requested_customer_id !== $user_customer_id ) {
return new WP_Error( 'rest_forbidden', 'You do not have permission to access this customer\'s pricing data.', array( 'status' => 403 ) );
}
// Optional: Further role-based checks can still be applied here if needed.
// For example, ensure the user also has a 'customer' role.
$user = wp_get_current_user();
$allowed_roles = array( 'customer', 'wholesale_customer', 'premium_customer', 'administrator' );
$has_allowed_role = false;
foreach ( $user->roles as $role ) {
if ( in_array( $role, $allowed_roles ) ) {
$has_allowed_role = true;
break;
}
}
if ( ! $has_allowed_role ) {
return new WP_Error( 'rest_forbidden', 'Your user role does not permit pricing access.', array( 'status' => 403 ) );
}
return true; // Permission granted.
}
?>
This revised permission check is much more robust:
- It first verifies if the user is logged in.
- It retrieves the `b2b_customer_id` meta for the current user.
- It compares the `customer_id` requested in the API call with the user’s assigned `b2b_customer_id`. If they don’t match, access is denied.
- It still includes the role check as a secondary layer of security.
Frontend Integration and Usage
On the frontend, you’ll need to make authenticated AJAX requests to your custom API endpoints. Ensure that the user is logged in and that their `b2b_customer_id` is available (e.g., passed via `wp_localize_script` or fetched separately).
// Example using jQuery for AJAX request
jQuery(document).ready(function($) {
var userId = wp_user_params.user_id; // Assuming user ID is available via wp_localize_script
var pricingApiUrl = '/wp-json/b2b-pricing/v1/grid/';
// Fetch user's assigned customer ID (e.g., from wp_localize_script or another API call)
// For this example, let's assume it's available as userCustomerData.customer_id
var userCustomerData = { customer_id: 'customer-abc' }; // Replace with actual fetched data
if (userId && userCustomerData.customer_id) {
var requestUrl = pricingApiUrl + userCustomerData.customer_id;
$.ajax({
url: requestUrl,
method: 'GET',
beforeSend: function(xhr) {
// Add nonce for authentication if needed, though REST API often uses cookies for logged-in users.
// xhr.setRequestHeader('X-WP-Nonce', wp_rest_params.nonce);
},
success: function(data) {
console.log('Pricing data:', data);
// Render pricing grid using 'data'
renderPricingGrid(data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error fetching pricing data:', textStatus, errorThrown);
// Display error message to the user
if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
alert('Error: ' + jqXHR.responseJSON.message);
} else {
alert('An unexpected error occurred.');
}
}
});
} else {
console.log('User not logged in or customer ID not available.');
// Redirect to login or show a message
}
function renderPricingGrid(pricingData) {
// Logic to dynamically build and display the pricing table
var html = '<table><thead><tr><th>Product</th><th>Price</th></tr></thead><tbody>';
for (var sku in pricingData.products) {
var product = pricingData.products[sku];
html += '<tr><td>' + sku + '</td><td>' + product.base_price.toFixed(2) + '</td></tr>';
// Add logic for tiers if needed
}
html += '</tbody></table>';
$('#pricing-grid-container').html(html); // Assuming a div with id="pricing-grid-container"
}
});
For frontend scripts, you’ll typically enqueue them and pass necessary data using `wp_localize_script`. This includes the REST API URL, nonce (if required for specific authentication methods), and potentially the current user’s ID or assigned customer ID.
Security Considerations and Best Practices
When dealing with pricing data, security is paramount. Always adhere to these principles:
- Never expose sensitive data directly. All access should be mediated by permission callbacks.
- Sanitize all inputs. Use functions like `sanitize_file_name`, `sanitize_text_field`, and `json_decode` with error checking.
- Validate data structures. Beyond basic JSON validation, ensure the pricing data itself conforms to expected formats and types.
- Use HTTPS. Essential for all API communication.
- Rate Limiting. Implement rate limiting on your API endpoints to prevent abuse. WordPress’s REST API has some built-in rate limiting, but you might need more aggressive measures.
- File Permissions. Ensure the `b2b-pricing-data` directory has appropriate file permissions (e.g., `755` for directories, `644` for files) and is not web-writable by default.
- Regular Audits. Periodically review user roles, meta data, and file access logs.
Conclusion
By combining WordPress’s REST API, the Filesystem API, and a robust role/user meta management strategy, you can build a secure and flexible B2B pricing grid system. Storing pricing data in JSON files managed by custom endpoints offers a performant and maintainable solution. The implementation of granular permission callbacks and user-specific customer ID assignments ensures that only authorized users can access the correct pricing information, fulfilling critical B2B e-commerce requirements.