diff --git a/backend/migrations/029_add_store_id_to_wp_api_permissions.sql b/backend/migrations/029_add_store_id_to_wp_api_permissions.sql new file mode 100644 index 00000000..c8d62892 --- /dev/null +++ b/backend/migrations/029_add_store_id_to_wp_api_permissions.sql @@ -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); diff --git a/backend/src/routes/api-permissions.ts b/backend/src/routes/api-permissions.ts index ad5b2c69..b01944ca 100644 --- a/backend/src/routes/api-permissions.ts +++ b/backend/src/routes/api-permissions.ts @@ -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({ diff --git a/frontend/public/wordpress/menus-v1.3.0.zip b/frontend/public/wordpress/menus-v1.3.0.zip new file mode 100644 index 00000000..668e3d17 Binary files /dev/null and b/frontend/public/wordpress/menus-v1.3.0.zip differ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 275e0ff7..107f7d4a 100755 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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), diff --git a/frontend/src/pages/ApiPermissions.tsx b/frontend/src/pages/ApiPermissions.tsx index 3e403ba4..a4e4670d 100644 --- a/frontend/src/pages/ApiPermissions.tsx +++ b/frontend/src/pages/ApiPermissions.tsx @@ -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([]); + const [stores, setStores] = useState([]); 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() {

A friendly name to identify this API user

+
+ + +

The store this API token can access

+
+