wardrobe-ai/wardrobe-ai/backend/server.js
OpenClaw Agent 390ec6e290 Address PR #34 review comments
- Fetch profile in fetchProfile() and use state in generateBox()
- Convert API underscore fields to camelCase for consistency
- Add TODO comment for auth gap in generateBox()
- Change CORS from '*' to specific allowed origins
2026-05-02 14:48:52 -07:00

346 lines
18 KiB
JavaScript

const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
// Amazon Affiliate Configuration
const AFFILIATE_ID = process.env.AMAZON_ASSOCIATE_ID || 'greybeardhold-20';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(express.json());
app.use(express.static(path.join(__dirname, '../frontend/public')));
app.use((req, res, next) => {
const allowedOrigins = ['http://localhost:3000', 'https://wardrobe.scoble.me'];
const origin = req.headers.origin || '';
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// Database initialization
const db = new sqlite3.Database(path.join(__dirname, 'wardrobe.db'), (err) => {
if (err) {
console.error('Error opening database:', err);
} else {
console.log('Connected to SQLite database');
}
});
// Create tables
db.serialize(() => {
// Users table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Style profiles table
db.run(`
CREATE TABLE IF NOT EXISTS style_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
sizes TEXT,
preferred_colors TEXT,
style_preferences TEXT,
lifestyle TEXT,
hard_lines TEXT,
must_haves TEXT,
budget INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`);
// Products table
db.run(`
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
asin TEXT UNIQUE NOT NULL,
title TEXT,
brand TEXT,
category TEXT,
color TEXT,
style_tags TEXT,
price REAL,
image_url TEXT,
affiliate_link TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Feedback table
db.run(`
CREATE TABLE IF NOT EXISTS feedback (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
would_keep INTEGER,
fit_rating INTEGER,
style_rating INTEGER,
color_rating INTEGER,
quality_rating INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)
`);
});
// Routes
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Get all products
app.get('/api/products', (req, res) => {
db.all('SELECT * FROM products', [], (err, rows) => {
if (err) {
console.error('Error fetching products:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
// Get products by category
app.get('/api/products/category/:category', (req, res) => {
db.all('SELECT * FROM products WHERE category LIKE ?', [`%${req.params.category}%`], (err, rows) => {
if (err) {
console.error('Error fetching products:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
// Create user
app.post('/api/users', (req, res) => {
const { email } = req.body;
db.run('INSERT INTO users (email) VALUES (?)', [email], function(err) {
if (err) {
console.error('Error creating user:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json({ id: this.lastID, email });
});
});
// Create style profile
app.post('/api/profiles', (req, res) => {
const { userId, sizes, preferredColors, stylePreferences, lifestyle, hardLines, mustHaves, budget } = req.body;
db.run(
'INSERT INTO style_profiles (user_id, sizes, preferred_colors, style_preferences, lifestyle, hard_lines, must_haves, budget) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[userId, JSON.stringify(sizes), JSON.stringify(preferredColors), JSON.stringify(stylePreferences), lifestyle, JSON.stringify(hardLines), JSON.stringify(mustHaves), budget],
function(err) {
if (err) {
console.error('Error creating profile:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json({ id: this.lastID, userId });
}
);
});
// Get style profile by user ID
app.get('/api/profile/:userId', (req, res) => {
db.get(
'SELECT * FROM style_profiles WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[req.params.userId],
(err, row) => {
if (err) {
console.error('Error fetching profile:', err);
return res.status(500).json({ error: 'Database error' });
}
if (!row) {
return res.status(404).json({ error: 'Profile not found' });
}
// Parse JSON fields
row.sizes = JSON.parse(row.sizes || '[]');
row.preferred_colors = JSON.parse(row.preferred_colors || '[]');
row.style_preferences = JSON.parse(row.style_preferences || '[]');
row.hard_lines = JSON.parse(row.hard_lines || '[]');
row.must_haves = JSON.parse(row.must_haves || '[]');
res.json(row);
}
);
});
// Generate curated box
app.post('/api/box', (req, res) => {
const { userId, preferredColors, stylePreferences, budget } = req.body;
// Validate and default stylePreferences to empty array
const styles = Array.isArray(stylePreferences) ? stylePreferences : (stylePreferences ? JSON.parse(stylePreferences) : []);
// Get user's hard lines
db.get(
'SELECT hard_lines FROM style_profiles WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
[userId],
(err, profile) => {
if (err) {
console.error('Error fetching profile:', err);
return res.status(500).json({ error: 'Database error' });
}
const hardLines = JSON.parse(profile?.hard_lines || '[]');
// Get all products
db.all('SELECT * FROM products', [], (err, products) => {
if (err) {
console.error('Error fetching products:', err);
return res.status(500).json({ error: 'Database error' });
}
// Score and filter products
const scoredProducts = products
.map(product => {
let score = 0;
// Color match: +25 points
if (preferredColors.includes(product.color)) {
score += 25;
}
// Style match: +35 points
if (styles.length > 0) {
let productStyleTags = JSON.parse(product.style_tags || '[]');
if (typeof productStyleTags === 'string') {
productStyleTags = productStyleTags.split(',').map(s => s.trim());
}
const matchingStyles = styles.filter(style =>
productStyleTags.some(tag => tag.toLowerCase().includes(style.toLowerCase()))
);
if (matchingStyles.length > 0) {
score += 35;
}
}
// Price fit: +20 points
if (product.price <= budget) {
score += 20;
}
return { ...product, score, matchingStyles: styles.filter(style => {
let tagList = JSON.parse(product.style_tags || '[]');
if (typeof tagList === 'string') tagList = tagList.split(',').map(s => s.trim());
return tagList.some(tag => tag.toLowerCase().includes(style.toLowerCase()));
}) };
})
.filter(product => {
// Filter out hard lines
const productStyleTags = JSON.parse(product.style_tags || '[]');
return !hardLines.some(hardLine =>
productStyleTags.some(tag => tag.toLowerCase().includes(hardLine.toLowerCase())) ||
product.title.toLowerCase().includes(hardLine.toLowerCase()) ||
product.brand.toLowerCase().includes(hardLine.toLowerCase())
);
})
.filter(product => product.price <= budget)
.sort((a, b) => b.score - a.score)
.slice(0, 6); // Top 4-6 items
res.json({
products: scoredProducts,
totalMatches: scoredProducts.length
});
});
}
);
});
// Submit feedback
app.post('/api/feedback', (req, res) => {
const { userId, productId, wouldKeep, fitRating, styleRating, colorRating, qualityRating } = req.body;
db.run(
'INSERT INTO feedback (user_id, product_id, would_keep, fit_rating, style_rating, color_rating, quality_rating) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, productId, wouldKeep ? 1 : 0, fitRating, styleRating, colorRating, qualityRating],
function(err) {
if (err) {
console.error('Error submitting feedback:', err);
return res.status(500).json({ error: 'Database error' });
}
res.json({ id: this.lastID });
}
);
});
// Load seed products
const loadSeedProducts = () => {
const seedProducts = [
{ asin: 'B08N5KWM3S', title: 'Hanes Women\'s EcoFleece Oversized Hoodie', brand: 'Hanes', category: 'Tops', color: 'Grey', style_tags: 'casual,comfortable,cozy', price: 22.99, image_url: 'https://m.media-amazon.com/images/I/61Z6J7X7X9L.jpg', affiliate_link: `https://www.amazon.com/dp/B08N5KWM3S?tag=${AFFILIATE_ID}` },
{ asin: 'B09Q5G7X1J', title: 'Gap Women\'s Classic French Terry Sweatpants', brand: 'Gap', category: 'Bottoms', color: 'Grey', style_tags: 'casual,comfortable,classic', price: 29.99, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X8L.jpg', affiliate_link: `https://www.amazon.com/dp/B09Q5G7X1J?tag=${AFFILIATE_ID}` },
{ asin: 'B08T9N5J1K', title: 'Levi\'s Women\'s 511 Slim Fit Jeans', brand: 'Levi\'s', category: 'Bottoms', color: 'Blue', style_tags: 'classic,professional,slim', price: 49.99, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X7L.jpg', affiliate_link: `https://www.amazon.com/dp/B08T9N5J1K?tag=${AFFILIATE_ID}` },
{ asin: 'B09R7P3X2K', title: 'Uniqlo Women\'s U-Sweater Crewneck', brand: 'Uniqlo', category: 'Tops', color: 'Navy', style_tags: 'classic,professional,casual', price: 39.90, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X6L.jpg', affiliate_link: `https://www.amazon.com/dp/B09R7P3X2K?tag=${AFFILIATE_ID}` },
{ asin: 'B08K7L8N1M', title: 'Amazon Essentials Women\'s Tanks', brand: 'Amazon Essentials', category: 'Tops', color: 'White', style_tags: 'casual,professional,classic', price: 12.99, image_url: 'https://m.media-amazon.com/images/I/51Z5J5X5X1L.jpg', affiliate_link: `https://www.amazon.com/dp/B08K7L8N1M?tag=${AFFILIATE_ID}` },
{ asin: 'B09M2N4X6P', title: 'Columbia Women\'s PFG Badgeless Shirt', brand: 'Columbia', category: 'Tops', color: 'Pink', style_tags: 'casual,outdoor,feminine', price: 49.99, image_url: 'https://m.media-amazon.com/images/I/61Z6J6X6X2L.jpg', affiliate_link: `https://www.amazon.com/dp/B09M2N4X6P?tag=${AFFILIATE_ID}` },
{ asin: 'B08F1K7L3N', title: 'Calvin Klein Women\'s Cotton Briefs', brand: 'Calvin Klein', category: 'Undergarments', color: 'White', style_tags: 'professional,classic,classic', price: 24.99, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X3L.jpg', affiliate_link: `https://www.amazon.com/dp/B08F1K7L3N?tag=${AFFILIATE_ID}` },
{ asin: 'B09D9R8K2L', title: 'Nike Women\'s Dri-FIT Running Socks', brand: 'Nike', category: 'Accessories', color: 'Black', style_tags: 'casual,active,feminine', price: 12.00, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X4L.jpg', affiliate_link: `https://www.amazon.com/dp/B09D9R8K2L?tag=${AFFILIATE_ID}` },
{ asin: 'B08YQJ7X5M', title: 'Wildfang Women\'s Arcade Hoodie', brand: 'Wildfang', category: 'Tops', color: 'Black', style_tags: 'edgy,casual,boho', price: 89.00, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X5L.jpg', affiliate_link: `https://www.amazon.com/dp/B08YQJ7X5M?tag=${AFFILIATE_ID}` },
{ asin: 'B09H1Z3X8P', title: 'Re/Done Women\'s Denim Jacket', brand: 'Re/Done', category: 'Outerwear', color: 'Blue', style_tags: 'classic,edgy,feminine', price: 228.00, image_url: 'https://m.media-amazon.com/images/I/61Z6J7X7X6L.jpg', affiliate_link: `https://www.amazon.com/dp/B09H1Z3X8P?tag=${AFFILIATE_ID}` },
{ asin: 'B08L5N9K2L', title: 'Free People Women\'s Macrofleurette Maxi Dress', brand: 'Free People', category: 'Dresses', color: 'Multicolor', style_tags: 'boho,feminine,casual', price: 79.00, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X7L.jpg', affiliate_link: `https://www.amazon.com/dp/B08L5N9K2L?tag=${AFFILIATE_ID}` },
{ asin: 'B09P7X6K1N', title: 'Anthropologie Women\'s Cottontail Maxi Dress', brand: 'Anthropologie', category: 'Dresses', color: 'White', style_tags: 'feminine,elegant,casual', price: 128.00, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X8L.jpg', affiliate_link: `https://www.amazon.com/dp/B09P7X6K1N?tag=${AFFILIATE_ID}` },
{ asin: 'B08Q4M7L3K', title: 'Madewell Women\'s Chino Shorts', brand: 'Madewell', category: 'Bottoms', color: 'Khaki', style_tags: 'casual,professional,classic', price: 44.00, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X1L.jpg', affiliate_link: `https://www.amazon.com/dp/B08Q4M7L3K?tag=${AFFILIATE_ID}` },
{ asin: 'B09W1Z5X3P', title: 'Eileen Fisher Women\'s Knit Tunic', brand: 'Eileen Fisher', category: 'Tops', color: 'Grey', style_tags: 'classic,professional,boho', price: 128.00, image_url: 'https://m.media-amazon.com/images/I/61Z6J6X6X3L.jpg', affiliate_link: `https://www.amazon.com/dp/B09W1Z5X3P?tag=${AFFILIATE_ID}` },
{ asin: 'B08J7K9L2N', title: 'Talbots Women\'s Cashmere Blend Sweater', brand: 'Talbots', category: 'Tops', color: 'Burgundy', style_tags: 'classic,professional,elegant', price: 128.00, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X4L.jpg', affiliate_link: `https://www.amazon.com/dp/B08J7K9L2N?tag=${AFFILIATE_ID}` },
{ asin: 'B09B3X8K1L', title: 'Banana Republic Women\'s Tailored Trousers', brand: 'Banana Republic', category: 'Bottoms', color: 'Black', style_tags: 'professional,classic,slim', price: 69.99, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X5L.jpg', affiliate_link: `https://www.amazon.com/dp/B09B3X8K1L?tag=${AFFILIATE_ID}` },
{ asin: 'B08X7N4K2M', title: 'J.Crew Women\'s Lucid Sateen Pants', brand: 'J.Crew', category: 'Bottoms', color: 'Navy', style_tags: 'professional,classic,slim', price: 59.99, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X2L.jpg', affiliate_link: `https://www.amazon.com/dp/B08X7N4K2M?tag=${AFFILIATE_ID}` },
{ asin: 'B09F2K6L1N', title: 'Old Navy Women\'s Power Stretch Jeans', brand: 'Old Navy', category: 'Bottoms', color: 'Blue', style_tags: 'casual,comfortable,classic', price: 29.99, image_url: 'https://m.media-amazon.com/images/I/61Z6J7X7X8L.jpg', affiliate_link: `https://www.amazon.com/dp/B09F2K6L1N?tag=${AFFILIATE_ID}` },
{ asin: 'B08R9X7K3L', title: 'Pact Women\'s Cotton Crew Tee', brand: 'Pact', category: 'Tops', color: 'Blue', style_tags: 'casual,eco-friendly,classic', price: 19.99, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X5L.jpg', affiliate_link: `https://www.amazon.com/dp/B08R9X7K3L?tag=${AFFILIATE_ID}` },
{ asin: 'B09L5Z2X4K', title: 'Pact Women\'s Cotton Cardigan', brand: 'Pact', category: 'Outerwear', color: 'Cream', style_tags: 'casual,eco-friendly,classic', price: 59.99, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X6L.jpg', affiliate_link: `https://www.amazon.com/dp/B09L5Z2X4K?tag=${AFFILIATE_ID}` },
{ asin: 'B08M1K8L2N', title: 'Girlfriend Collective Women\'s Compress Leggings', brand: 'Girlfriend Collective', category: 'Bottoms', color: 'Black', style_tags: 'active,edgy,casual', price: 68.00, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X3L.jpg', affiliate_link: `https://www.amazon.com/dp/B08M1K8L2N?tag=${AFFILIATE_ID}` },
{ asin: 'B09Q2K5L1M', title: 'Reversible Running Cap', brand: 'Amazon Basics', category: 'Accessories', color: 'Black', style_tags: 'active,casual,practical', price: 9.99, image_url: 'https://m.media-amazon.com/images/I/61Z6J6X6X4L.jpg', affiliate_link: `https://www.amazon.com/dp/B09Q2K5L1M?tag=${AFFILIATE_ID}` },
{ asin: 'B08T7X9K1L', title: 'Bamboo Silk Scarf', brand: 'Etsy', category: 'Accessories', color: 'Multicolor', style_tags: 'boho,feminine,elegant', price: 24.99, image_url: 'https://m.media-amazon.com/images/I/71X7Y7Z7X6L.jpg', affiliate_link: `https://www.amazon.com/dp/B08T7X9K1L?tag=${AFFILIATE_ID}` },
{ asin: 'B09D1Z4K2P', title: 'Leather Tote Bag', brand: 'Amazon Basics', category: 'Accessories', color: 'Brown', style_tags: 'professional,classic,practical', price: 39.99, image_url: 'https://m.media-amazon.com/images/I/81X8Z8Z8X7L.jpg', affiliate_link: `https://www.amazon.com/dp/B09D1Z4K2P?tag=${AFFILIATE_ID}` },
{ asin: 'B08W3K7L1N', title: 'Sunglasses - Oversized', brand: 'Amazon Basics', category: 'Accessories', color: 'Black', style_tags: 'feminine,edgy,casual', price: 14.99, image_url: 'https://m.media-amazon.com/images/I/91X9Z9Z9X4L.jpg', affiliate_link: `https://www.amazon.com/dp/B08W3K7L1N?tag=${AFFILIATE_ID}` },
{ asin: 'B09H5X2K3L', title: 'Beaded Necklace Set', brand: 'Etsy', category: 'Accessories', color: 'Multicolor', style_tags: 'boho,feminine,elegant', price: 29.99, image_url: 'https://m.media-amazon.com/images/I/61Z6J7X7X9L.jpg', affiliate_link: `https://www.amazon.com/dp/B09H5X2K3L?tag=${AFFILIATE_ID}` }
];
seedProducts.forEach(product => {
db.run(
`INSERT OR IGNORE INTO products (asin, title, brand, category, color, style_tags, price, image_url, affiliate_link)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
product.asin,
product.title,
product.brand,
product.category,
product.color,
JSON.stringify(product.style_tags),
product.price,
product.image_url,
product.affiliate_link
],
(err) => {
if (err) console.error('Error inserting product:', err);
}
);
});
};
// Initialize database and load seed data
loadSeedProducts();
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});