272 lines
8.2 KiB
JavaScript
272 lines
8.2 KiB
JavaScript
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(`
|
||
<html>
|
||
<body style="font-family: Arial; text-align: center; padding: 50px;">
|
||
<h1>✅ Salla Connected!</h1>
|
||
<p>Authorization successful. You can close this window.</p>
|
||
<p>Tokens have been logged to the console.</p>
|
||
<script>setTimeout(() => window.close(), 3000);</script>
|
||
</body>
|
||
</html>
|
||
`);
|
||
} 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');
|
||
}
|
||
});
|