require('dotenv').config(); const express = require('express'); const cors = require('cors'); const axios = require('axios'); const app = express(); app.use(cors()); app.use(express.json()); const PORT = process.env.SALLA_SERVER_PORT || 3001; // Salla OAuth Config const SALLA_CLIENT_ID = process.env.SALLA_CLIENT_ID; const SALLA_CLIENT_SECRET = process.env.SALLA_CLIENT_SECRET; const SALLA_REDIRECT_URI = process.env.SALLA_REDIRECT_URI || 'http://localhost:3001/auth/callback'; // Token storage (in production, use a database) let accessToken = process.env.SALLA_ACCESS_TOKEN || null; let refreshToken = process.env.SALLA_REFRESH_TOKEN || null; // ============================================ // OAuth Endpoints // ============================================ // State for CSRF protection const crypto = require('crypto'); let oauthState = null; // Step 1: Redirect to Salla authorization app.get('/auth/login', (req, res) => { oauthState = crypto.randomBytes(16).toString('hex'); const authUrl = `https://accounts.salla.sa/oauth2/auth?` + `client_id=${SALLA_CLIENT_ID}` + `&redirect_uri=${encodeURIComponent(SALLA_REDIRECT_URI)}` + `&response_type=code` + `&scope=offline_access` + `&state=${oauthState}`; res.redirect(authUrl); }); // Step 2: Handle OAuth callback app.get('/auth/callback', async (req, res) => { const { code, error, state } = req.query; if (error) { return res.status(400).json({ error: 'Authorization denied', details: error }); } if (!code) { return res.status(400).json({ error: 'No authorization code received' }); } // Verify state (optional check - some flows may not return state) if (oauthState && state && state !== oauthState) { return res.status(400).json({ error: 'Invalid state parameter' }); } try { // Exchange code for tokens const response = await axios.post('https://accounts.salla.sa/oauth2/token', { client_id: SALLA_CLIENT_ID, client_secret: SALLA_CLIENT_SECRET, grant_type: 'authorization_code', code: code, redirect_uri: SALLA_REDIRECT_URI }, { headers: { 'Content-Type': 'application/json' } }); accessToken = response.data.access_token; refreshToken = response.data.refresh_token; // Log tokens (save these to .env for persistence) console.log('\n========================================'); console.log('šŸŽ‰ SALLA CONNECTED SUCCESSFULLY!'); console.log('========================================'); console.log('Add these to your .env file:'); console.log(`SALLA_ACCESS_TOKEN=${accessToken}`); console.log(`SALLA_REFRESH_TOKEN=${refreshToken}`); console.log('========================================\n'); res.send(`

āœ… Salla Connected!

Authorization successful. You can close this window.

Tokens have been logged to the console.

`); } catch (err) { console.error('Token exchange failed:', err.response?.data || err.message); res.status(500).json({ error: 'Token exchange failed', details: err.response?.data }); } }); // Check auth status app.get('/auth/status', (req, res) => { res.json({ connected: !!accessToken, hasRefreshToken: !!refreshToken }); }); // Refresh token async function refreshAccessToken() { if (!refreshToken) throw new Error('No refresh token available'); const response = await axios.post('https://accounts.salla.sa/oauth2/token', { client_id: SALLA_CLIENT_ID, client_secret: SALLA_CLIENT_SECRET, grant_type: 'refresh_token', refresh_token: refreshToken }); accessToken = response.data.access_token; if (response.data.refresh_token) { refreshToken = response.data.refresh_token; } return accessToken; } // ============================================ // Salla API Proxy Endpoints // ============================================ // Generic API caller with auto-refresh async function callSallaAPI(endpoint, method = 'GET', data = null) { if (!accessToken) throw new Error('Not authenticated. Visit /auth/login first.'); try { const response = await axios({ method, url: `https://api.salla.dev/admin/v2${endpoint}`, headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, data }); return response.data; } catch (err) { if (err.response?.status === 401) { // Try refresh await refreshAccessToken(); return callSallaAPI(endpoint, method, data); } throw err; } } // Get store info app.get('/api/store', async (req, res) => { try { const data = await callSallaAPI('/store/info'); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get orders app.get('/api/orders', async (req, res) => { try { const { page = 1, per_page = 50, status } = req.query; let endpoint = `/orders?page=${page}&per_page=${per_page}`; if (status) endpoint += `&status=${status}`; const data = await callSallaAPI(endpoint); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get order details app.get('/api/orders/:id', async (req, res) => { try { const data = await callSallaAPI(`/orders/${req.params.id}`); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get products app.get('/api/products', async (req, res) => { try { const { page = 1, per_page = 50 } = req.query; const data = await callSallaAPI(`/products?page=${page}&per_page=${per_page}`); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get customers app.get('/api/customers', async (req, res) => { try { const { page = 1, per_page = 50 } = req.query; const data = await callSallaAPI(`/customers?page=${page}&per_page=${per_page}`); res.json(data); } catch (err) { res.status(500).json({ error: err.message }); } }); // Get analytics/reports app.get('/api/analytics/summary', async (req, res) => { try { // Fetch multiple endpoints for a summary const [orders, products] = await Promise.all([ callSallaAPI('/orders?per_page=100'), callSallaAPI('/products?per_page=100') ]); // Calculate summary const ordersList = orders.data || []; const totalRevenue = ordersList.reduce((sum, o) => sum + (o.amounts?.total?.amount || 0), 0); const avgOrderValue = ordersList.length > 0 ? totalRevenue / ordersList.length : 0; res.json({ orders: { total: orders.pagination?.total || ordersList.length, recent: ordersList.length }, products: { total: products.pagination?.total || (products.data?.length || 0) }, revenue: { total: totalRevenue, average_order: avgOrderValue, currency: ordersList[0]?.amounts?.total?.currency || 'SAR' } }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ============================================ // Start Server // ============================================ app.listen(PORT, () => { console.log(`\nšŸš€ Salla Integration Server running on http://localhost:${PORT}`); console.log('\nEndpoints:'); console.log(' GET /auth/login - Start OAuth flow'); console.log(' GET /auth/callback - OAuth callback'); console.log(' GET /auth/status - Check connection status'); console.log(' GET /api/store - Store info'); console.log(' GET /api/orders - List orders'); console.log(' GET /api/products - List products'); console.log(' GET /api/customers - List customers'); console.log(' GET /api/analytics/summary - Dashboard summary'); if (!SALLA_CLIENT_ID || !SALLA_CLIENT_SECRET) { console.log('\nāš ļø WARNING: SALLA_CLIENT_ID and SALLA_CLIENT_SECRET not set!'); console.log(' Add them to server/.env file'); } if (accessToken) { console.log('\nāœ… Access token loaded from environment'); } else { console.log('\nšŸ“ Visit http://localhost:' + PORT + '/auth/login to connect Salla'); } });