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:
@@ -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);
|
||||
@@ -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({
|
||||
|
||||
BIN
frontend/public/wordpress/menus-v1.3.0.zip
Normal file
BIN
frontend/public/wordpress/menus-v1.3.0.zip
Normal file
Binary file not shown.
@@ -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),
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user