Add store selection to API permissions

- Add store_id and store_name columns to wp_dutchie_api_permissions
- Backend: Add /stores endpoint, require store_id when creating permissions
- Frontend: Add store selector dropdown to API Permissions form
- WordPress plugin v1.3.0: Remove store_id from shortcodes (store is tied to token)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Kelly
2025-12-01 13:59:01 -07:00
parent d2635ed123
commit e345707db2
6 changed files with 109 additions and 10 deletions

View File

@@ -0,0 +1,13 @@
-- Migration: Add store_id to wp_dutchie_api_permissions
-- This allows API tokens to be associated with a specific store
-- Add store_id column to wp_dutchie_api_permissions
ALTER TABLE wp_dutchie_api_permissions
ADD COLUMN IF NOT EXISTS store_id INTEGER REFERENCES stores(id);
-- Add index for faster lookups
CREATE INDEX IF NOT EXISTS idx_wp_api_permissions_store_id ON wp_dutchie_api_permissions(store_id);
-- Add store_name column to return store info without join
ALTER TABLE wp_dutchie_api_permissions
ADD COLUMN IF NOT EXISTS store_name VARCHAR(255);

View File

@@ -27,6 +27,21 @@ router.get('/', requireRole('superadmin', 'admin'), async (req, res) => {
}
});
// Get all stores for dropdown (must be before /:id to avoid route conflict)
router.get('/stores', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const result = await pool.query(`
SELECT id, name
FROM stores
ORDER BY name
`);
res.json({ stores: result.rows });
} catch (error) {
console.error('Error fetching stores:', error);
res.status(500).json({ error: 'Failed to fetch stores' });
}
});
// Get single API permission
router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
try {
@@ -52,12 +67,23 @@ router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
// Create new API permission
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
try {
const { user_name, allowed_ips, allowed_domains } = req.body;
const { user_name, allowed_ips, allowed_domains, store_id } = req.body;
if (!user_name) {
return res.status(400).json({ error: 'User name is required' });
}
if (!store_id) {
return res.status(400).json({ error: 'Store is required' });
}
// Get store name for display
const storeResult = await pool.query('SELECT name FROM stores WHERE id = $1', [store_id]);
if (storeResult.rows.length === 0) {
return res.status(400).json({ error: 'Invalid store ID' });
}
const storeName = storeResult.rows[0].name;
const apiKey = generateApiKey();
const result = await pool.query(`
@@ -66,15 +92,19 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
api_key,
allowed_ips,
allowed_domains,
is_active
is_active,
store_id,
store_name
)
VALUES ($1, $2, $3, $4, 1)
VALUES ($1, $2, $3, $4, 1, $5, $6)
RETURNING *
`, [
user_name,
apiKey,
allowed_ips || null,
allowed_domains || null
allowed_domains || null,
store_id,
storeName
]);
res.status(201).json({

Binary file not shown.

View File

@@ -398,7 +398,11 @@ class ApiClient {
return this.request<{ permissions: any[] }>('/api/api-permissions');
}
async createApiPermission(data: { user_name: string; allowed_ips?: string; allowed_domains?: string }) {
async getApiPermissionStores() {
return this.request<{ stores: Array<{ id: number; name: string }> }>('/api/api-permissions/stores');
}
async createApiPermission(data: { user_name: string; store_id: number; allowed_ips?: string; allowed_domains?: string }) {
return this.request<{ permission: any; message: string }>('/api/api-permissions', {
method: 'POST',
body: JSON.stringify(data),

View File

@@ -12,14 +12,23 @@ interface ApiPermission {
is_active: number;
created_at: string;
last_used_at: string | null;
store_id: number | null;
store_name: string | null;
}
interface Store {
id: number;
name: string;
}
export function ApiPermissions() {
const [permissions, setPermissions] = useState<ApiPermission[]>([]);
const [stores, setStores] = useState<Store[]>([]);
const [loading, setLoading] = useState(true);
const [showAddForm, setShowAddForm] = useState(false);
const [newPermission, setNewPermission] = useState({
user_name: '',
store_id: '',
allowed_ips: '',
allowed_domains: '',
});
@@ -27,8 +36,18 @@ export function ApiPermissions() {
useEffect(() => {
loadPermissions();
loadStores();
}, []);
const loadStores = async () => {
try {
const data = await api.getApiPermissionStores();
setStores(data.stores);
} catch (error: any) {
console.error('Failed to load stores:', error);
}
};
const loadPermissions = async () => {
setLoading(true);
try {
@@ -49,10 +68,18 @@ export function ApiPermissions() {
return;
}
if (!newPermission.store_id) {
setNotification({ message: 'Store is required', type: 'error' });
return;
}
try {
const result = await api.createApiPermission(newPermission);
const result = await api.createApiPermission({
...newPermission,
store_id: parseInt(newPermission.store_id),
});
setNotification({ message: result.message, type: 'success' });
setNewPermission({ user_name: '', allowed_ips: '', allowed_domains: '' });
setNewPermission({ user_name: '', store_id: '', allowed_ips: '', allowed_domains: '' });
setShowAddForm(false);
loadPermissions();
} catch (error: any) {
@@ -153,6 +180,26 @@ export function ApiPermissions() {
<p className="text-sm text-gray-600 mt-1">A friendly name to identify this API user</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Store *
</label>
<select
value={newPermission.store_id}
onChange={(e) => setNewPermission({ ...newPermission, store_id: e.target.value })}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
required
>
<option value="">Select a store...</option>
{stores.map((store) => (
<option key={store.id} value={store.id}>
{store.name}
</option>
))}
</select>
<p className="text-sm text-gray-600 mt-1">The store this API token can access</p>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Allowed IP Addresses
@@ -213,6 +260,9 @@ export function ApiPermissions() {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
User Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Store
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
API Key
</th>
@@ -239,6 +289,9 @@ export function ApiPermissions() {
<td className="px-6 py-4 whitespace-nowrap">
<div className="font-medium text-gray-900">{perm.user_name}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{perm.store_name || <span className="text-gray-400 italic">No store</span>}</div>
</td>
<td className="px-6 py-4">
<div className="flex items-center space-x-2">
<code className="text-xs bg-gray-100 px-2 py-1 rounded">

View File

@@ -3,7 +3,7 @@
* Plugin Name: Dutchie Menus
* Plugin URI: https://creationshop.io
* Description: Display cannabis product menus from your Dutchie scraper with Elementor integration
* Version: 1.2.0
* Version: 1.3.0
* Author: Creationshop
* Author URI: https://creationshop.io
* License: GPL v2 or later
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
define('DUTCHIE_MENUS_VERSION', '1.2.0');
define('DUTCHIE_MENUS_VERSION', '1.3.0');
define('DUTCHIE_MENUS_API_URL', 'https://dispos.crawlsy.com/api');
define('DUTCHIE_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('DUTCHIE_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
@@ -177,7 +177,6 @@ class Dutchie_Menus_Plugin {
*/
public function products_shortcode($atts) {
$atts = shortcode_atts([
'store_id' => get_option('dutchie_default_store_id', 1),
'category_id' => '',
'limit' => 12,
'columns' => 3,