Files
hihala-dashboard/server/index.js
2026-02-16 10:59:55 +03:00

272 lines
8.2 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
}
});