last changes
This commit is contained in:
271
server/index.js
Normal file
271
server/index.js
Normal file
@@ -0,0 +1,271 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user