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
|
// Get single API permission
|
||||||
router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
|
router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -52,12 +67,23 @@ router.get('/:id', requireRole('superadmin', 'admin'), async (req, res) => {
|
|||||||
// Create new API permission
|
// Create new API permission
|
||||||
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { user_name, allowed_ips, allowed_domains } = req.body;
|
const { user_name, allowed_ips, allowed_domains, store_id } = req.body;
|
||||||
|
|
||||||
if (!user_name) {
|
if (!user_name) {
|
||||||
return res.status(400).json({ error: 'User name is required' });
|
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 apiKey = generateApiKey();
|
||||||
|
|
||||||
const result = await pool.query(`
|
const result = await pool.query(`
|
||||||
@@ -66,15 +92,19 @@ router.post('/', requireRole('superadmin', 'admin'), async (req, res) => {
|
|||||||
api_key,
|
api_key,
|
||||||
allowed_ips,
|
allowed_ips,
|
||||||
allowed_domains,
|
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 *
|
RETURNING *
|
||||||
`, [
|
`, [
|
||||||
user_name,
|
user_name,
|
||||||
apiKey,
|
apiKey,
|
||||||
allowed_ips || null,
|
allowed_ips || null,
|
||||||
allowed_domains || null
|
allowed_domains || null,
|
||||||
|
store_id,
|
||||||
|
storeName
|
||||||
]);
|
]);
|
||||||
|
|
||||||
res.status(201).json({
|
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');
|
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', {
|
return this.request<{ permission: any; message: string }>('/api/api-permissions', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|||||||
@@ -12,14 +12,23 @@ interface ApiPermission {
|
|||||||
is_active: number;
|
is_active: number;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
last_used_at: string | null;
|
last_used_at: string | null;
|
||||||
|
store_id: number | null;
|
||||||
|
store_name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ApiPermissions() {
|
export function ApiPermissions() {
|
||||||
const [permissions, setPermissions] = useState<ApiPermission[]>([]);
|
const [permissions, setPermissions] = useState<ApiPermission[]>([]);
|
||||||
|
const [stores, setStores] = useState<Store[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
const [newPermission, setNewPermission] = useState({
|
const [newPermission, setNewPermission] = useState({
|
||||||
user_name: '',
|
user_name: '',
|
||||||
|
store_id: '',
|
||||||
allowed_ips: '',
|
allowed_ips: '',
|
||||||
allowed_domains: '',
|
allowed_domains: '',
|
||||||
});
|
});
|
||||||
@@ -27,8 +36,18 @@ export function ApiPermissions() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPermissions();
|
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 () => {
|
const loadPermissions = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -49,10 +68,18 @@ export function ApiPermissions() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!newPermission.store_id) {
|
||||||
|
setNotification({ message: 'Store is required', type: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
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' });
|
setNotification({ message: result.message, type: 'success' });
|
||||||
setNewPermission({ user_name: '', allowed_ips: '', allowed_domains: '' });
|
setNewPermission({ user_name: '', store_id: '', allowed_ips: '', allowed_domains: '' });
|
||||||
setShowAddForm(false);
|
setShowAddForm(false);
|
||||||
loadPermissions();
|
loadPermissions();
|
||||||
} catch (error: any) {
|
} 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>
|
<p className="text-sm text-gray-600 mt-1">A friendly name to identify this API user</p>
|
||||||
</div>
|
</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">
|
<div className="mb-4">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Allowed IP Addresses
|
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">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
User Name
|
User Name
|
||||||
</th>
|
</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">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
API Key
|
API Key
|
||||||
</th>
|
</th>
|
||||||
@@ -239,6 +289,9 @@ export function ApiPermissions() {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="font-medium text-gray-900">{perm.user_name}</div>
|
<div className="font-medium text-gray-900">{perm.user_name}</div>
|
||||||
</td>
|
</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">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
|
<code className="text-xs bg-gray-100 px-2 py-1 rounded">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: Dutchie Menus
|
* Plugin Name: Dutchie Menus
|
||||||
* Plugin URI: https://creationshop.io
|
* Plugin URI: https://creationshop.io
|
||||||
* Description: Display cannabis product menus from your Dutchie scraper with Elementor integration
|
* Description: Display cannabis product menus from your Dutchie scraper with Elementor integration
|
||||||
* Version: 1.2.0
|
* Version: 1.3.0
|
||||||
* Author: Creationshop
|
* Author: Creationshop
|
||||||
* Author URI: https://creationshop.io
|
* Author URI: https://creationshop.io
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
|
|||||||
exit; // Exit if accessed directly
|
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_API_URL', 'https://dispos.crawlsy.com/api');
|
||||||
define('DUTCHIE_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('DUTCHIE_MENUS_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('DUTCHIE_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('DUTCHIE_MENUS_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
@@ -177,7 +177,6 @@ class Dutchie_Menus_Plugin {
|
|||||||
*/
|
*/
|
||||||
public function products_shortcode($atts) {
|
public function products_shortcode($atts) {
|
||||||
$atts = shortcode_atts([
|
$atts = shortcode_atts([
|
||||||
'store_id' => get_option('dutchie_default_store_id', 1),
|
|
||||||
'category_id' => '',
|
'category_id' => '',
|
||||||
'limit' => 12,
|
'limit' => 12,
|
||||||
'columns' => 3,
|
'columns' => 3,
|
||||||
|
|||||||
Reference in New Issue
Block a user