feat: setup S2S integration, tailwind fixes, admin routing, nginx fallback

This commit is contained in:
2026-09-22 16:40:42 +05:30
parent 45d13fa7be
commit 05358be6c1
17 changed files with 522 additions and 149 deletions
+118 -38
View File
@@ -1,5 +1,7 @@
const axios = require('axios');
const FormData = require('form-data');
const Config = require('../models/Config');
const { decrypt } = require('../utils/crypto');
exports.analyzeImage = async (req, res) => {
try {
@@ -7,50 +9,128 @@ exports.analyzeImage = async (req, res) => {
return res.status(400).json({ error: 'No image provided' });
}
// In a real scenario, we would forward this to the Perfect Corp API
// For now, we will return a mock response matching the expected structure
// or you could proxy it like this:
/*
const formData = new FormData();
formData.append('image', req.file.buffer, req.file.originalname);
const response = await axios.post(process.env.PERFECT_CORP_API_URL, formData, {
headers: {
...formData.getHeaders(),
'Authorization': `Bearer ${process.env.PERFECT_CORP_API_KEY}`
}
});
return res.json({ result: response.data });
*/
// Fetch API Key from DB
const apiKeyConfig = await Config.findOne({ keyName: 'PERFECT_CORP_API_KEY' });
if (!apiKeyConfig) {
return res.status(500).json({ error: 'API Key configuration missing' });
}
const decryptedApiKey = decrypt(apiKeyConfig.encryptedValue, apiKeyConfig.iv);
// MOCK RESPONSE
console.log('Mocking perfect corp analysis for image:', req.file.originalname);
const mockResult = {
results: [
const baseUrl = process.env.PERFECT_CORP_API_URL || 'https://yce-api-01.makeupar.com/s2s/v2.0';
// Step 1: Initialize Upload
console.log('Step 1: Initializing Upload...');
const initPayload = {
files: [
{
data: [
{
url: 'https://example.com/mock-mask.zip'
}
]
content_type: req.file.mimetype || 'image/jpeg',
file_name: req.file.originalname || 'image.jpg',
file_size: req.file.size
}
],
// Add other mock data that the frontend expects to parse...
face_attributes: {
age: 30,
gender: 'female'
},
skin_issues: {
wrinkles: 10,
spots: 5,
acne: 2
}
]
};
res.json({ result: mockResult });
const initResponse = await axios.post(`${baseUrl}/file`, initPayload, {
headers: {
'Authorization': `Bearer ${decryptedApiKey}`,
'Content-Type': 'application/json'
}
});
const fileData = initResponse.data?.data?.files?.[0];
if (!fileData || !fileData.requests || !fileData.requests[0]) {
throw new Error('Failed to get upload URL from Perfect Corp API');
}
const fileId = fileData.file_id;
const uploadRequest = fileData.requests[0];
// Step 2: S3 Upload
console.log(`Step 2: Uploading image to S3 (fileId: ${fileId})...`);
await axios.put(uploadRequest.url, req.file.buffer, {
headers: uploadRequest.headers
});
// Step 3: Create Task
console.log('Step 3: Creating AI Task...');
const taskPayload = {
src_file_id: fileId,
dst_actions: [
"wrinkle", "pore", "texture", "acne", "moisture", "eye_bag",
"firmness", "age_spot", "radiance", "redness", "oiliness"
],
miniserver_args: {
enable_mask_overlay: false // Generate multiple output masks
},
format: "json"
};
const taskResponse = await axios.post(`${baseUrl}/task/skin-analysis`, taskPayload, {
headers: {
'Authorization': `Bearer ${decryptedApiKey}`,
'Content-Type': 'application/json'
}
});
const taskId = taskResponse.data?.data?.task_id;
if (!taskId) {
throw new Error('Failed to create AI task');
}
// Step 4: Poll Task Status
console.log(`Step 4: Polling task status for taskId: ${taskId}...`);
let taskFinished = false;
let finalResult = null;
let attempts = 0;
const maxAttempts = 30; // 60 seconds max
while (!taskFinished && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2 seconds
attempts++;
const pollResponse = await axios.get(`${baseUrl}/task/skin-analysis/${taskId}`, {
headers: {
'Authorization': `Bearer ${decryptedApiKey}`
}
});
const status = pollResponse.data?.data?.task_status;
if (status === 'success') {
taskFinished = true;
finalResult = pollResponse.data;
} else if (status === 'error' || status === 'failed') {
throw new Error(`AI Task failed with status: ${status}`);
} else {
console.log(`Polling... (status: ${status}, attempt: ${attempts})`);
}
}
if (!taskFinished) {
throw new Error('AI Task polling timed out');
}
console.log('Task finished successfully!');
// Format output to mirror the score_info.json structure
const outputArray = finalResult?.data?.results?.output || [];
const formattedScores = {};
for (const item of outputArray) {
if (item.type) {
formattedScores[item.type] = {
value: item.raw_score || item.ui_score || 0,
ui_score: item.ui_score,
raw_score: item.raw_score,
output_mask_name: (item.mask_urls && item.mask_urls.length > 0) ? item.mask_urls[0] : null
};
}
}
// Send formatted scores back to frontend
res.json({ result: { scores: formattedScores, raw_response: finalResult } });
} catch (error) {
console.error('Error analyzing image:', error);
console.error('Error analyzing image:', error.response ? JSON.stringify(error.response.data) : error.message);
res.status(500).json({ error: 'Failed to analyze image' });
}
};
+35
View File
@@ -41,3 +41,38 @@ exports.getReport = async (req, res) => {
res.status(500).json({ success: false, message: 'Server error' });
}
};
exports.savePdfReport = async (req, res) => {
try {
const { phone, userName } = req.body;
if (!phone || !req.file) {
return res.status(400).json({ success: false, message: 'Missing phone or PDF file' });
}
// Ensure user exists
let user = await User.findOne({ phone });
if (!user) {
user = await User.create({ phone, name: userName || 'Unknown' });
} else if (userName && userName !== user.name) {
user.name = userName;
await user.save();
}
const reportId = Math.random().toString(36).substring(7);
const link = `/uploads/reports/${req.file.filename}`;
const report = await Report.create({
reportId,
phone,
userName: user.name,
data: { message: "PDF Report generated." }, // Minimal data to satisfy schema
link
});
res.status(201).json({ success: true, link, reportId });
} catch (error) {
console.error('Error saving PDF report:', error);
res.status(500).json({ success: false, message: 'Server error' });
}
};
+25
View File
@@ -0,0 +1,25 @@
const mongoose = require('mongoose');
const configSchema = new mongoose.Schema(
{
keyName: {
type: String,
required: true,
unique: true,
},
encryptedValue: {
type: String,
required: true,
},
iv: {
type: String, // Initialization vector needed for decryption
required: true,
}
},
{
timestamps: true,
}
);
const Config = mongoose.model('Config', configSchema);
module.exports = Config;
+21 -1
View File
@@ -1,14 +1,33 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const fs = require('fs');
// Configure multer for memory storage (we'll forward the buffer to the external API)
const upload = multer({ storage: multer.memoryStorage() });
// Ensure uploads dir exists for PDF reports
const reportsDir = path.join(__dirname, '../uploads/reports');
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
const diskStorage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, reportsDir)
},
filename: function (req, file, cb) {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)
cb(null, (req.body.phone || 'anonymous') + '-' + uniqueSuffix + '.pdf')
}
});
const uploadPdf = multer({ storage: diskStorage });
// Controllers
const { analyzeImage } = require('../controllers/analysisController');
const { createUser, verifyOtp } = require('../controllers/userController');
const { createReport, getReport } = require('../controllers/reportController');
const { createReport, getReport, savePdfReport } = require('../controllers/reportController');
const { sendOtp, sendReport } = require('../controllers/whatsappController');
const { getDashboardUsers } = require('../controllers/dashboardController');
@@ -22,6 +41,7 @@ router.post('/verify-otp', verifyOtp);
// 3. Reports
router.post('/reports', createReport);
router.get('/reports/:reportId', getReport);
router.post('/reports/save-pdf', uploadPdf.single('pdf'), savePdfReport);
// 4. WhatsApp Integration
router.post('/whatsapp/send-otp', sendOtp);
+42
View File
@@ -0,0 +1,42 @@
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config({ path: path.join(__dirname, '../.env') });
const Config = require('../models/Config');
const { encrypt } = require('../utils/crypto');
const PERFECT_CORP_API_KEY = 'sk-tHtPU8rst9cifrM6RhkjCKy9EpNfXBdcoCbzP0QDRhIkHr_InBYPS6g_6XGSumbm';
const PERFECT_CORP_SECRET_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCKexD/nyLrGq7jXCxYi/Tjwr297cZS5EUFQbCV9NO2XQU/sJitsT2WBHHS8AYAQrzM56hPgnwpt/B+EuSuLfVbM/ympBUvSrULUNlrJF5lW96JMd1KF8RWC9fICZJuCE2iGpGAd0l9pOCUmYM4JZ/n26HxZxSSramkoOYxmhFYhQIDAQAB';
async function seedKeys() {
try {
await mongoose.connect(process.env.MONGO_URI || 'mongodb://mongo:27017/limelite');
console.log('Connected to DB');
// 1. API KEY
const encryptedApi = encrypt(PERFECT_CORP_API_KEY);
await Config.findOneAndUpdate(
{ keyName: 'PERFECT_CORP_API_KEY' },
{ encryptedValue: encryptedApi.encryptedData, iv: encryptedApi.iv },
{ upsert: true, new: true }
);
// 2. SECRET KEY
const encryptedSecret = encrypt(PERFECT_CORP_SECRET_KEY);
await Config.findOneAndUpdate(
{ keyName: 'PERFECT_CORP_SECRET_KEY' },
{ encryptedValue: encryptedSecret.encryptedData, iv: encryptedSecret.iv },
{ upsert: true, new: true }
);
console.log('Successfully encrypted and stored Perfect Corp API keys in the database.');
process.exit(0);
} catch (error) {
console.error('Error seeding keys:', error);
process.exit(1);
}
}
seedKeys();
+2
View File
@@ -2,6 +2,7 @@ const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv');
const connectDB = require('./config/db');
const path = require('path');
// Load env vars
dotenv.config();
@@ -14,6 +15,7 @@ const app = express();
// Middleware
app.use(cors());
app.use(express.json());
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Routes
const apiRoutes = require('./routes/api');
+34
View File
@@ -0,0 +1,34 @@
const crypto = require('crypto');
// The encryption key must be exactly 32 bytes for AES-256
const getEncryptionKey = () => {
const key = process.env.ENCRYPTION_KEY || 'default_secret_key_needs_32_bytes!';
// Ensure it's 32 bytes by hashing it if it's not exactly 32 bytes
if (key.length !== 32) {
return crypto.createHash('sha256').update(key).digest();
}
return Buffer.from(key);
};
const algorithm = 'aes-256-cbc';
function encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, getEncryptionKey(), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
iv: iv.toString('hex'),
encryptedData: encrypted
};
}
function decrypt(encryptedData, ivHex) {
const iv = Buffer.from(ivHex, 'hex');
const decipher = crypto.createDecipheriv(algorithm, getEncryptionKey(), iv);
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
module.exports = { encrypt, decrypt };