updateweb
This commit is contained in:
@@ -10,3 +10,10 @@ DB_PASSWORD=SkyArt2025Pass
|
||||
SESSION_SECRET=skyart-shop-secret-2025-change-this-in-production
|
||||
|
||||
UPLOAD_DIR=/var/www/SkyArtShop/wwwroot/uploads/images
|
||||
|
||||
# New structure variables
|
||||
DATABASE_URL="postgresql://skyartapp:SkyArt2025Pass@localhost:5432/skyartshop?schema=public"
|
||||
JWT_SECRET=skyart-shop-secret-2025-change-this-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
MAX_FILE_SIZE=5242880
|
||||
|
||||
19
backend/.env.example
Normal file
19
backend/.env.example
Normal file
@@ -0,0 +1,19 @@
|
||||
# Environment Variables for Backend
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# Server
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/skyartshop?schema=public"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# CORS
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
|
||||
# Upload
|
||||
MAX_FILE_SIZE=5242880
|
||||
30
backend/.gitignore
vendored
Normal file
30
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Uploads
|
||||
uploads/
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
346
backend/add-customer-service-pages.js
Normal file
346
backend/add-customer-service-pages.js
Normal file
@@ -0,0 +1,346 @@
|
||||
const db = require("./config/database");
|
||||
|
||||
async function addCustomerServicePages() {
|
||||
try {
|
||||
console.log("Adding customer service pages to database...\n");
|
||||
|
||||
// Helper function to insert or update page
|
||||
async function upsertPage(slug, title, html, metatitle, metadescription) {
|
||||
const existing = await db.query("SELECT id FROM pages WHERE slug = $1", [
|
||||
slug,
|
||||
]);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
await db.query(
|
||||
`
|
||||
UPDATE pages
|
||||
SET pagecontent = $1, content = $1, title = $2, metatitle = $3, metadescription = $4, updatedat = NOW()
|
||||
WHERE slug = $5
|
||||
`,
|
||||
[html, title, metatitle, metadescription, slug]
|
||||
);
|
||||
console.log(`✓ ${title} page updated`);
|
||||
} else {
|
||||
await db.query(
|
||||
`
|
||||
INSERT INTO pages (id, slug, title, content, pagecontent, metatitle, metadescription, ispublished, isactive, createdat, updatedat)
|
||||
VALUES (gen_random_uuid()::text, $1, $2, $3, $3, $4, $5, true, true, NOW(), NOW())
|
||||
`,
|
||||
[slug, title, html, metatitle, metadescription]
|
||||
);
|
||||
console.log(`✓ ${title} page added`);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Shipping Info Page
|
||||
const shippingHTML = `
|
||||
<div style="max-width: 1000px; margin: 0 auto; padding: 40px 20px;">
|
||||
<div style="text-align: center; margin-bottom: 60px;">
|
||||
<h2 style="font-size: 2.5rem; font-weight: 700; color: #202023; margin-bottom: 16px;">
|
||||
Shipping Information
|
||||
</h2>
|
||||
<p style="font-size: 1.1rem; color: #202023; opacity: 0.7; max-width: 700px; margin: 0 auto;">
|
||||
Everything you need to know about our shipping policies and delivery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-truck" style="font-size: 1.5rem;"></i> Shipping Methods
|
||||
</h3>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;"><strong>Standard Shipping:</strong> 5-7 business days - $5.99</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;"><strong>Express Shipping:</strong> 2-3 business days - $12.99</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;"><strong>Priority Overnight:</strong> 1 business day - $24.99</li>
|
||||
<li style="padding: 12px 0;"><strong>Free Shipping:</strong> Orders over $50 (Standard shipping)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-geo-alt" style="font-size: 1.5rem;"></i> Delivery Areas
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
We currently ship to all 50 states in the United States. International shipping is available to Canada, UK, and Australia. Additional fees may apply for international orders.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-clock" style="font-size: 1.5rem;"></i> Processing Time
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Orders are typically processed within 1-2 business days. You will receive a tracking number via email once your order ships. Custom or personalized items may require additional processing time (3-5 business days).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await upsertPage(
|
||||
"shipping-info",
|
||||
"Shipping Info",
|
||||
shippingHTML,
|
||||
"Shipping Information - Sky Art Shop",
|
||||
"Learn about our shipping methods, delivery times, and policies."
|
||||
);
|
||||
|
||||
// 2. Returns Page
|
||||
const returnsHTML = `
|
||||
<div style="max-width: 1000px; margin: 0 auto; padding: 40px 20px;">
|
||||
<div style="text-align: center; margin-bottom: 60px;">
|
||||
<h2 style="font-size: 2.5rem; font-weight: 700; color: #202023; margin-bottom: 16px;">
|
||||
Returns & Refunds
|
||||
</h2>
|
||||
<p style="font-size: 1.1rem; color: #202023; opacity: 0.7; max-width: 700px; margin: 0 auto;">
|
||||
Our hassle-free return policy to ensure your satisfaction
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-arrow-counterclockwise" style="font-size: 1.5rem;"></i> Return Policy
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 20px; font-size: 1.05rem;">
|
||||
We accept returns within <strong>30 days</strong> of purchase. Items must be in original condition, unused, and in original packaging.
|
||||
</p>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">✓ Item must be unused and in original condition</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">✓ Original packaging must be intact</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">✓ Include receipt or proof of purchase</li>
|
||||
<li style="padding: 12px 0;">✓ Custom or personalized items cannot be returned</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-cash-coin" style="font-size: 1.5rem;"></i> Refund Process
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Once we receive your return, we will inspect the item and process your refund within 5-7 business days. Refunds will be issued to the original payment method. Shipping costs are non-refundable unless the return is due to our error.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px; display: flex; align-items: center; gap: 12px;">
|
||||
<i class="bi bi-box-seam" style="font-size: 1.5rem;"></i> How to Return
|
||||
</h3>
|
||||
<ol style="line-height: 2; color: #202023; font-size: 1.05rem; padding-left: 20px;">
|
||||
<li style="padding: 8px 0;">Contact us at <a href="mailto:returns@skyartshop.com" style="color: #FCB1D8; text-decoration: none; font-weight: 500;">returns@skyartshop.com</a> to initiate a return</li>
|
||||
<li style="padding: 8px 0;">Pack the item securely in original packaging</li>
|
||||
<li style="padding: 8px 0;">Ship to the address provided in our return confirmation email</li>
|
||||
<li style="padding: 8px 0;">We recommend using a trackable shipping method</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await upsertPage(
|
||||
"returns",
|
||||
"Returns",
|
||||
returnsHTML,
|
||||
"Returns & Refunds - Sky Art Shop",
|
||||
"Our return policy and refund process explained."
|
||||
);
|
||||
|
||||
// 3. FAQ Page
|
||||
const faqHTML = `
|
||||
<div style="max-width: 1000px; margin: 0 auto; padding: 40px 20px;">
|
||||
<div style="text-align: center; margin-bottom: 60px;">
|
||||
<h2 style="font-size: 2.5rem; font-weight: 700; color: #202023; margin-bottom: 16px;">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
<p style="font-size: 1.1rem; color: #202023; opacity: 0.7; max-width: 700px; margin: 0 auto;">
|
||||
Find answers to common questions about our products and services
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 40px; padding-bottom: 40px; border-bottom: 2px solid #FFD0D0;">
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> How do I place an order?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Simply browse our shop, add items to your cart, and proceed to checkout. You can pay securely with credit card, debit card, or PayPal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 40px; padding-bottom: 40px; border-bottom: 2px solid #FFD0D0;">
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> Do you offer custom artwork?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Yes! We offer custom commissions for paintings and artwork. Contact us with your vision and we'll provide a quote and timeline.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 40px; padding-bottom: 40px; border-bottom: 2px solid #FFD0D0;">
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> How long does shipping take?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Standard shipping takes 5-7 business days. Express shipping (2-3 days) and overnight options are available. Processing time is 1-2 business days.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 40px; padding-bottom: 40px; border-bottom: 2px solid #FFD0D0;">
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> What payment methods do you accept?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
We accept all major credit cards (Visa, Mastercard, American Express, Discover), debit cards, and PayPal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 40px; padding-bottom: 40px; border-bottom: 2px solid #FFD0D0;">
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> Can I cancel or modify my order?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
You can cancel or modify your order within 24 hours of placing it. Contact us immediately at <a href="mailto:contact@skyartshop.com" style="color: #FCB1D8; text-decoration: none; font-weight: 500;">contact@skyartshop.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="font-size: 1.4rem; font-weight: 600; color: #FCB1D8; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;">
|
||||
<i class="bi bi-question-circle" style="font-size: 1.3rem;"></i> Do you ship internationally?
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
Yes, we ship to Canada, UK, and Australia. International shipping costs vary by location and are calculated at checkout.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await upsertPage(
|
||||
"faq",
|
||||
"FAQ",
|
||||
faqHTML,
|
||||
"Frequently Asked Questions - Sky Art Shop",
|
||||
"Answers to common questions about orders, shipping, and our services."
|
||||
);
|
||||
|
||||
// 4. Privacy Policy Page
|
||||
const privacyHTML = `
|
||||
<div style="max-width: 1000px; margin: 0 auto; padding: 40px 20px;">
|
||||
<div style="text-align: center; margin-bottom: 60px;">
|
||||
<h2 style="font-size: 2.5rem; font-weight: 700; color: #202023; margin-bottom: 16px;">
|
||||
Privacy Policy
|
||||
</h2>
|
||||
<p style="font-size: 1.1rem; color: #202023; opacity: 0.7; max-width: 700px; margin: 0 auto;">
|
||||
How we collect, use, and protect your information
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
Information We Collect
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 16px; font-size: 1.05rem;">
|
||||
We collect information you provide directly to us, including:
|
||||
</p>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Name, email address, and contact information</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Billing and shipping addresses</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Payment information (processed securely)</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Order history and preferences</li>
|
||||
<li style="padding: 12px 0;">• Communications with our customer service</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
How We Use Your Information
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 16px; font-size: 1.05rem;">
|
||||
We use the information we collect to:
|
||||
</p>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Process and fulfill your orders</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Communicate with you about your orders</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Send promotional emails (with your consent)</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Improve our website and services</li>
|
||||
<li style="padding: 12px 0;">• Prevent fraud and enhance security</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
Information Sharing
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 16px; font-size: 1.05rem;">
|
||||
We do not sell your personal information. We may share your information with:
|
||||
</p>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Service providers who help us operate our business</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Payment processors for secure transactions</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Shipping companies to deliver your orders</li>
|
||||
<li style="padding: 12px 0;">• Law enforcement when required by law</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
Data Security
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
We implement appropriate security measures to protect your personal information. All payment information is encrypted using SSL technology. However, no method of transmission over the internet is 100% secure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 50px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
Your Rights
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 16px; font-size: 1.05rem;">
|
||||
You have the right to:
|
||||
</p>
|
||||
<ul style="line-height: 2; color: #202023; font-size: 1.05rem; list-style: none; padding: 0;">
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Access your personal information</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Correct inaccurate information</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Request deletion of your data</li>
|
||||
<li style="padding: 12px 0; border-bottom: 1px solid #FFD0D0;">• Opt-out of marketing communications</li>
|
||||
<li style="padding: 12px 0;">• Lodge a complaint with a supervisory authority</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="padding: 30px; background: #FFEBEB; border-left: 4px solid #FCB1D8; border-radius: 8px;">
|
||||
<h3 style="font-size: 1.75rem; font-weight: 600; color: #FCB1D8; margin-bottom: 20px;">
|
||||
Contact Us
|
||||
</h3>
|
||||
<p style="line-height: 1.8; color: #202023; margin-bottom: 16px; font-size: 1.05rem;">
|
||||
If you have questions about this Privacy Policy, please contact us at:
|
||||
</p>
|
||||
<p style="line-height: 1.8; color: #202023; font-size: 1.05rem;">
|
||||
<strong>Email:</strong> <a href="mailto:privacy@skyartshop.com" style="color: #FCB1D8; text-decoration: none; font-weight: 500;">privacy@skyartshop.com</a><br>
|
||||
<strong>Phone:</strong> +1 (555) 123-4567<br>
|
||||
<strong>Last Updated:</strong> January 1, 2026
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await upsertPage(
|
||||
"privacy",
|
||||
"Privacy Policy",
|
||||
privacyHTML,
|
||||
"Privacy Policy - Sky Art Shop",
|
||||
"Our privacy policy and how we protect your information."
|
||||
);
|
||||
|
||||
console.log("\n✅ All customer service pages added successfully!");
|
||||
console.log("\nPages available at:");
|
||||
console.log(" - http://localhost:5000/shipping-info.html");
|
||||
console.log(" - http://localhost:5000/returns.html");
|
||||
console.log(" - http://localhost:5000/faq.html");
|
||||
console.log(" - http://localhost:5000/privacy.html");
|
||||
console.log(
|
||||
"\nThese pages are now editable in the admin panel under Custom Pages!"
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error adding customer service pages:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addCustomerServicePages();
|
||||
@@ -1,147 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Add Test Portfolio Projects
|
||||
* This script adds sample portfolio projects to the database
|
||||
*/
|
||||
|
||||
const { query } = require("./config/database");
|
||||
|
||||
async function addTestPortfolioProjects() {
|
||||
console.log("🎨 Adding test portfolio projects...\n");
|
||||
|
||||
const testProjects = [
|
||||
{
|
||||
title: "Sunset Landscape Series",
|
||||
description: `<h2>A Beautiful Collection of Sunset Landscapes</h2>
|
||||
<p>This series captures the breathtaking beauty of sunsets across different landscapes. Each piece showcases unique color palettes ranging from warm oranges and reds to cool purples and blues.</p>
|
||||
<h3>Key Features:</h3>
|
||||
<ul>
|
||||
<li>High-resolution digital paintings</li>
|
||||
<li>Vibrant color gradients</li>
|
||||
<li>Emotional depth and atmosphere</li>
|
||||
<li>Available in multiple sizes</li>
|
||||
</ul>
|
||||
<p><strong>Medium:</strong> Digital Art<br>
|
||||
<strong>Year:</strong> 2024<br>
|
||||
<strong>Collection:</strong> Nature Series</p>`,
|
||||
category: "Digital Art",
|
||||
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
|
||||
isactive: true,
|
||||
},
|
||||
{
|
||||
title: "Abstract Geometric Patterns",
|
||||
description: `<h2>Modern Abstract Compositions</h2>
|
||||
<p>A collection of abstract artworks featuring <strong>bold geometric patterns</strong> and contemporary design elements. These pieces explore the relationship between shape, color, and space.</p>
|
||||
<h3>Artistic Approach:</h3>
|
||||
<ol>
|
||||
<li>Started with basic geometric shapes</li>
|
||||
<li>Layered multiple patterns and textures</li>
|
||||
<li>Applied vibrant color combinations</li>
|
||||
<li>Refined composition for visual balance</li>
|
||||
</ol>
|
||||
<p><em>These works are inspired by modernist movements and contemporary design trends.</em></p>`,
|
||||
category: "Abstract",
|
||||
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
|
||||
isactive: true,
|
||||
},
|
||||
{
|
||||
title: "Portrait Photography Collection",
|
||||
description: `<h2>Capturing Human Emotion</h2>
|
||||
<p>This portrait series explores the <strong>depth of human emotion</strong> through carefully composed photographs. Each subject tells a unique story through their expression and body language.</p>
|
||||
<h3>Technical Details:</h3>
|
||||
<ul>
|
||||
<li><strong>Camera:</strong> Canon EOS R5</li>
|
||||
<li><strong>Lens:</strong> 85mm f/1.4</li>
|
||||
<li><strong>Lighting:</strong> Natural and studio</li>
|
||||
<li><strong>Processing:</strong> Adobe Lightroom & Photoshop</li>
|
||||
</ul>
|
||||
<p>Shot in various locations including urban settings, nature, and professional studios.</p>`,
|
||||
category: "Photography",
|
||||
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
|
||||
isactive: true,
|
||||
},
|
||||
{
|
||||
title: "Watercolor Botanical Illustrations",
|
||||
description: `<h2>Delicate Flora Studies</h2>
|
||||
<p>A series of <em>hand-painted watercolor illustrations</em> featuring various botanical subjects. These pieces celebrate the intricate beauty of plants and flowers.</p>
|
||||
<h3>Collection Includes:</h3>
|
||||
<ul>
|
||||
<li>Wildflowers and garden blooms</li>
|
||||
<li>Tropical plants and leaves</li>
|
||||
<li>Herbs and medicinal plants</li>
|
||||
<li>Seasonal botanical studies</li>
|
||||
</ul>
|
||||
<blockquote>
|
||||
<p>"Nature always wears the colors of the spirit." - Ralph Waldo Emerson</p>
|
||||
</blockquote>
|
||||
<p>Each illustration is created using professional-grade watercolors on cold-press paper.</p>`,
|
||||
category: "Illustration",
|
||||
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
|
||||
isactive: true,
|
||||
},
|
||||
{
|
||||
title: "Urban Architecture Study",
|
||||
description: `<h2>Modern Cityscapes and Structures</h2>
|
||||
<p>An exploration of <strong>contemporary urban architecture</strong> through the lens of artistic photography and digital manipulation.</p>
|
||||
<h3>Focus Areas:</h3>
|
||||
<ul>
|
||||
<li>Geometric building facades</li>
|
||||
<li>Glass and steel structures</li>
|
||||
<li>Reflections and symmetry</li>
|
||||
<li>Night photography and lighting</li>
|
||||
</ul>
|
||||
<p>This project was completed over 6 months, documenting various cities and their unique architectural personalities.</p>
|
||||
<p><strong>Featured Cities:</strong> New York, Tokyo, Dubai, London</p>`,
|
||||
category: "Photography",
|
||||
imageurl: "/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg",
|
||||
isactive: false,
|
||||
},
|
||||
];
|
||||
|
||||
try {
|
||||
// Get next ID - portfolioprojects.id appears to be text/varchar type
|
||||
const maxIdResult = await query(
|
||||
"SELECT MAX(CAST(id AS INTEGER)) as max_id FROM portfolioprojects WHERE id ~ '^[0-9]+$'"
|
||||
);
|
||||
let nextId = (maxIdResult.rows[0].max_id || 0) + 1;
|
||||
|
||||
for (const project of testProjects) {
|
||||
const result = await query(
|
||||
`INSERT INTO portfolioprojects (id, title, description, category, imageurl, isactive, createdat, updatedat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
RETURNING id, title`,
|
||||
[
|
||||
nextId.toString(),
|
||||
project.title,
|
||||
project.description,
|
||||
project.category,
|
||||
project.imageurl,
|
||||
project.isactive,
|
||||
]
|
||||
);
|
||||
|
||||
console.log(
|
||||
`✓ Created: "${result.rows[0].title}" (ID: ${result.rows[0].id})`
|
||||
);
|
||||
nextId++;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n🎉 Successfully added ${testProjects.length} test portfolio projects!`
|
||||
);
|
||||
console.log("\n📝 Note: All projects use a placeholder image. You can:");
|
||||
console.log(" 1. Go to /admin/portfolio.html");
|
||||
console.log(" 2. Edit each project");
|
||||
console.log(" 3. Select real images from your media library");
|
||||
console.log("\n✅ Portfolio management is now ready to use!\n");
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("❌ Error adding test projects:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the function
|
||||
addTestPortfolioProjects();
|
||||
13
backend/biome.json
Normal file
13
backend/biome.json
Normal file
@@ -0,0 +1,13 @@
|
||||
$schema: https://biomejs.dev/schemas/1.4.1/schema.json
|
||||
|
||||
linter:
|
||||
enabled: true
|
||||
rules:
|
||||
recommended: true
|
||||
|
||||
formatter:
|
||||
enabled: true
|
||||
formatWithErrors: false
|
||||
indentStyle: space
|
||||
indentWidth: 2
|
||||
lineWidth: 100
|
||||
143
backend/middleware/cache.js
Normal file
143
backend/middleware/cache.js
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* In-Memory Cache Middleware
|
||||
* Caches API responses to reduce database load
|
||||
*/
|
||||
const logger = require("../config/logger");
|
||||
|
||||
class CacheManager {
|
||||
constructor(defaultTTL = 300000) {
|
||||
// 5 minutes default
|
||||
this.cache = new Map();
|
||||
this.defaultTTL = defaultTTL;
|
||||
}
|
||||
|
||||
set(key, value, ttl = this.defaultTTL) {
|
||||
const expiresAt = Date.now() + ttl;
|
||||
this.cache.set(key, { value, expiresAt });
|
||||
logger.debug(`Cache set: ${key} (TTL: ${ttl}ms)`);
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const cached = this.cache.get(key);
|
||||
if (!cached) return null;
|
||||
|
||||
if (Date.now() > cached.expiresAt) {
|
||||
this.cache.delete(key);
|
||||
logger.debug(`Cache expired: ${key}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.debug(`Cache hit: ${key}`);
|
||||
return cached.value;
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
const deleted = this.cache.delete(key);
|
||||
if (deleted) logger.debug(`Cache invalidated: ${key}`);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
deletePattern(pattern) {
|
||||
let count = 0;
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.includes(pattern)) {
|
||||
this.cache.delete(key);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0)
|
||||
logger.debug(`Cache pattern invalidated: ${pattern} (${count} keys)`);
|
||||
return count;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const size = this.cache.size;
|
||||
this.cache.clear();
|
||||
logger.info(`Cache cleared (${size} keys)`);
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
// Clean up expired entries
|
||||
cleanup() {
|
||||
const now = Date.now();
|
||||
let cleaned = 0;
|
||||
for (const [key, { expiresAt }] of this.cache.entries()) {
|
||||
if (now > expiresAt) {
|
||||
this.cache.delete(key);
|
||||
cleaned++;
|
||||
}
|
||||
}
|
||||
if (cleaned > 0)
|
||||
logger.debug(`Cache cleanup: removed ${cleaned} expired entries`);
|
||||
return cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
// Global cache instance
|
||||
const cache = new CacheManager();
|
||||
|
||||
// Cleanup interval reference (for graceful shutdown)
|
||||
let cleanupInterval = null;
|
||||
|
||||
// Start automatic cleanup (optional, call from server startup)
|
||||
const startCleanup = () => {
|
||||
if (!cleanupInterval) {
|
||||
cleanupInterval = setInterval(() => cache.cleanup(), 300000); // 5 minutes
|
||||
logger.debug("Cache cleanup scheduler started");
|
||||
}
|
||||
};
|
||||
|
||||
// Stop automatic cleanup (for graceful shutdown)
|
||||
const stopCleanup = () => {
|
||||
if (cleanupInterval) {
|
||||
clearInterval(cleanupInterval);
|
||||
cleanupInterval = null;
|
||||
logger.debug("Cache cleanup scheduler stopped");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache middleware factory
|
||||
* @param {number} ttl - Time to live in milliseconds
|
||||
* @param {function} keyGenerator - Function to generate cache key from req
|
||||
*/
|
||||
const cacheMiddleware = (ttl = 300000, keyGenerator = null) => {
|
||||
return (req, res, next) => {
|
||||
// Skip cache for authenticated requests
|
||||
if (req.session && req.session.userId) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const key = keyGenerator
|
||||
? keyGenerator(req)
|
||||
: `${req.method}:${req.originalUrl}`;
|
||||
|
||||
const cachedResponse = cache.get(key);
|
||||
if (cachedResponse) {
|
||||
res.setHeader("X-Cache", "HIT");
|
||||
return res.json(cachedResponse);
|
||||
}
|
||||
|
||||
// Store original json method
|
||||
const originalJson = res.json.bind(res);
|
||||
|
||||
// Override json method to cache response
|
||||
res.json = function (data) {
|
||||
cache.set(key, data, ttl);
|
||||
res.setHeader("X-Cache", "MISS");
|
||||
return originalJson(data);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
cache,
|
||||
cacheMiddleware,
|
||||
startCleanup,
|
||||
stopCleanup,
|
||||
};
|
||||
35
backend/middleware/compression.js
Normal file
35
backend/middleware/compression.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Response Compression Middleware
|
||||
* Compresses API responses to reduce payload size
|
||||
*/
|
||||
const compression = require("compression");
|
||||
|
||||
const compressionMiddleware = compression({
|
||||
// Only compress responses larger than 1kb
|
||||
threshold: 1024,
|
||||
// Compression level (0-9, higher = better compression but slower)
|
||||
level: 6,
|
||||
// Filter function - don't compress already compressed formats
|
||||
filter: (req, res) => {
|
||||
if (req.headers["x-no-compression"]) {
|
||||
return false;
|
||||
}
|
||||
// Check content-type
|
||||
const contentType = res.getHeader("Content-Type");
|
||||
if (!contentType) return compression.filter(req, res);
|
||||
|
||||
// Don't compress images, videos, or already compressed formats
|
||||
if (
|
||||
contentType.includes("image/") ||
|
||||
contentType.includes("video/") ||
|
||||
contentType.includes("application/zip") ||
|
||||
contentType.includes("application/pdf")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = compressionMiddleware;
|
||||
457
backend/node_modules/.package-lock.json
generated
vendored
457
backend/node_modules/.package-lock.json
generated
vendored
@@ -44,6 +44,75 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.1.tgz",
|
||||
"integrity": "sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz",
|
||||
"integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz",
|
||||
"integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/fetch-engine": "5.7.1",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz",
|
||||
"integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz",
|
||||
"integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz",
|
||||
"integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
@@ -123,6 +192,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
@@ -195,6 +278,19 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@@ -228,6 +324,19 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -283,6 +392,31 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
@@ -347,6 +481,45 @@
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -728,6 +901,19 @@
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@@ -888,6 +1074,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -922,6 +1121,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1029,6 +1238,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -1064,6 +1280,29 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1073,6 +1312,29 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -1363,6 +1625,84 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
"integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"nodemon": "bin/nodemon.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -1378,6 +1718,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
@@ -1571,6 +1921,19 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
@@ -1610,6 +1973,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz",
|
||||
"integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.7.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -1629,6 +2010,13 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -1698,6 +2086,19 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -1985,6 +2386,19 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@@ -2061,6 +2475,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
@@ -2096,6 +2523,19 @@
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -2105,6 +2545,16 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -2151,6 +2601,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
-- Add site_settings table for storing configuration
|
||||
CREATE TABLE IF NOT EXISTS site_settings (
|
||||
key VARCHAR(100) PRIMARY KEY,
|
||||
settings JSONB NOT NULL DEFAULT '{}',
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
updatedat TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Add indexes for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_site_settings_key ON site_settings(key);
|
||||
|
||||
-- Insert default settings if they don't exist
|
||||
INSERT INTO site_settings (key, settings, createdat, updatedat)
|
||||
VALUES
|
||||
('general', '{}', NOW(), NOW()),
|
||||
('homepage', '{}', NOW(), NOW()),
|
||||
('menu', '{"items":[]}', NOW(), NOW())
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
|
||||
-- Ensure products table has all necessary columns
|
||||
ALTER TABLE products
|
||||
ADD COLUMN IF NOT EXISTS isbestseller BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS category VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
|
||||
|
||||
-- Ensure portfolioprojects table has all necessary columns
|
||||
ALTER TABLE portfolioprojects
|
||||
ADD COLUMN IF NOT EXISTS category VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS isactive BOOLEAN DEFAULT TRUE,
|
||||
ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
|
||||
|
||||
-- Ensure blogposts table has all necessary columns
|
||||
ALTER TABLE blogposts
|
||||
ADD COLUMN IF NOT EXISTS metatitle VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS metadescription TEXT,
|
||||
ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
|
||||
|
||||
-- Ensure pages table has all necessary columns
|
||||
ALTER TABLE pages
|
||||
ADD COLUMN IF NOT EXISTS metatitle VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS metadescription TEXT,
|
||||
ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
|
||||
|
||||
-- Ensure adminusers table has all necessary columns
|
||||
ALTER TABLE adminusers
|
||||
ADD COLUMN IF NOT EXISTS name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS username VARCHAR(255) UNIQUE,
|
||||
ADD COLUMN IF NOT EXISTS passwordneverexpires BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS updatedat TIMESTAMP DEFAULT NOW();
|
||||
|
||||
-- Add username for existing users if not exists
|
||||
UPDATE adminusers
|
||||
SET username = LOWER(REGEXP_REPLACE(email, '@.*$', ''))
|
||||
WHERE username IS NULL;
|
||||
|
||||
-- Add name for existing users if not exists
|
||||
UPDATE adminusers
|
||||
SET name = INITCAP(REGEXP_REPLACE(email, '@.*$', ''))
|
||||
WHERE name IS NULL;
|
||||
|
||||
COMMENT ON TABLE site_settings IS 'Stores site-wide configuration settings in JSON format';
|
||||
COMMENT ON TABLE products IS 'Product catalog with variants and inventory';
|
||||
COMMENT ON TABLE portfolioprojects IS 'Portfolio showcase projects';
|
||||
COMMENT ON TABLE blogposts IS 'Blog posts with SEO metadata';
|
||||
COMMENT ON TABLE pages IS 'Custom pages with SEO metadata';
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo " Server Port Status - 192.168.10.130"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "🌐 Web Services:"
|
||||
echo "----------------------------------------"
|
||||
check_port() {
|
||||
local port=$1
|
||||
local service=$2
|
||||
local expected_pid=$3
|
||||
|
||||
if ss -tln | grep -q ":$port "; then
|
||||
local process=$(sudo lsof -i :$port -t 2>/dev/null | head -1)
|
||||
local cmd=$(ps -p $process -o comm= 2>/dev/null)
|
||||
echo " ✅ Port $port ($service) - $cmd [PID: $process]"
|
||||
else
|
||||
echo " ❌ Port $port ($service) - NOT LISTENING"
|
||||
fi
|
||||
}
|
||||
|
||||
check_port 80 "HTTP/nginx"
|
||||
check_port 443 "HTTPS/nginx"
|
||||
check_port 5000 "SkyArtShop Backend"
|
||||
check_port 8080 "House of Prayer"
|
||||
check_port 3000 "HOP Frontend"
|
||||
|
||||
echo ""
|
||||
echo "💾 Database Services:"
|
||||
echo "----------------------------------------"
|
||||
check_port 3306 "MySQL/MariaDB"
|
||||
check_port 5432 "PostgreSQL"
|
||||
|
||||
echo ""
|
||||
echo "🔍 Checking for Port Conflicts:"
|
||||
echo "----------------------------------------"
|
||||
|
||||
# Check for duplicate SkyArtShop instances
|
||||
SKYART_COUNT=$(ps aux | grep -c "/var/www/SkyArtShop/backend/server.js" | grep -v grep)
|
||||
if [ "$SKYART_COUNT" -gt 1 ]; then
|
||||
echo " ⚠️ Multiple SkyArtShop instances detected!"
|
||||
ps aux | grep "/var/www/SkyArtShop/backend/server.js" | grep -v grep
|
||||
else
|
||||
echo " ✅ No duplicate SkyArtShop instances"
|
||||
fi
|
||||
|
||||
# Check if port 3001 is free (should be)
|
||||
if ss -tln | grep -q ":3001 "; then
|
||||
echo " ⚠️ Port 3001 still in use (should be free)"
|
||||
sudo lsof -i :3001
|
||||
else
|
||||
echo " ✅ Port 3001 is free (old instance cleaned up)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📊 All Active Ports:"
|
||||
echo "----------------------------------------"
|
||||
ss -tlnp 2>/dev/null | grep LISTEN | awk '{print $4}' | grep -o "[0-9]*$" | sort -n | uniq | head -20
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Summary"
|
||||
echo "=========================================="
|
||||
echo " SkyArtShop: Port 5000 ✓"
|
||||
echo " House of Prayer: Port 8080 ✓"
|
||||
echo " Nginx HTTPS: Port 443 ✓"
|
||||
echo " PostgreSQL: Port 5432 ✓"
|
||||
echo ""
|
||||
echo "Run this script anytime to check port status."
|
||||
echo "=========================================="
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "========================================="
|
||||
echo "Checking SkyArtShop Backend Status"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
|
||||
# Check if database tables exist
|
||||
echo "1. Checking database tables..."
|
||||
PGPASSWORD=SkyArt2025Pass! psql -U skyartapp -d skyartshop -c "\dt" 2>&1 | grep -E "adminusers|appusers|session|No relations"
|
||||
|
||||
echo ""
|
||||
echo "2. Checking if adminusers table exists and count..."
|
||||
PGPASSWORD=SkyArt2025Pass! psql -U skyartapp -d skyartshop -c "SELECT COUNT(*) FROM adminusers;" 2>&1
|
||||
|
||||
echo ""
|
||||
echo "3. Listing all admin users..."
|
||||
PGPASSWORD=SkyArt2025Pass! psql -U skyartapp -d skyartshop -c "SELECT id, email, name, role, createdat FROM adminusers;" 2>&1
|
||||
|
||||
echo ""
|
||||
echo "4. Checking if Node.js backend is running..."
|
||||
ps aux | grep "node.*server.js" | grep -v grep
|
||||
|
||||
echo ""
|
||||
echo "5. Checking if port 3001 is in use..."
|
||||
netstat -tlnp 2>/dev/null | grep :3001 || ss -tlnp 2>/dev/null | grep :3001 || echo "Port 3001 not in use"
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo " SkyArtShop System Status"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Check backend process
|
||||
echo "✓ Backend Process:"
|
||||
ps aux | grep "node server.js" | grep SkyArtShop | grep -v grep | awk '{print " PID: "$2" | Command: node server.js"}'
|
||||
|
||||
# Check port 5000
|
||||
echo ""
|
||||
echo "✓ Port 5000 (Backend):"
|
||||
ss -tlnp 2>/dev/null | grep :5000 | awk '{print " "$1" "$4}'
|
||||
|
||||
# Check nginx
|
||||
echo ""
|
||||
echo "✓ Nginx Status:"
|
||||
sudo systemctl is-active nginx
|
||||
sudo nginx -t 2>&1 | grep "successful"
|
||||
|
||||
# Check database connection
|
||||
echo ""
|
||||
echo "✓ Database Connection:"
|
||||
PGPASSWORD='SkyArt2025Pass' psql -h localhost -U skyartapp -d skyartshop -c "SELECT COUNT(*) as admin_users FROM adminusers;" 2>/dev/null
|
||||
|
||||
# Test endpoints
|
||||
echo ""
|
||||
echo "✓ Health Check:"
|
||||
curl -s http://localhost:5000/health | jq -r '" Status: \(.status) | Database: \(.database)"' 2>/dev/null || echo " OK"
|
||||
|
||||
echo ""
|
||||
echo "✓ Admin Login Page:"
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/admin/login)
|
||||
if [ "$STATUS" == "200" ]; then
|
||||
echo " HTTP $STATUS - OK"
|
||||
else
|
||||
echo " HTTP $STATUS - ERROR"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Login Credentials"
|
||||
echo "=========================================="
|
||||
echo " URL: http://localhost/admin/login"
|
||||
echo " or http://skyarts.ddns.net/admin/login"
|
||||
echo ""
|
||||
echo " Email: admin@example.com"
|
||||
echo " Password: Admin123"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Backend is running on PORT 5000 ✓"
|
||||
echo "Nginx is proxying localhost:5000 ✓"
|
||||
echo "All .NET components have been replaced ✓"
|
||||
echo ""
|
||||
@@ -1,94 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Complete setup and troubleshooting script
|
||||
|
||||
cd /var/www/SkyArtShop/backend
|
||||
|
||||
echo "================================================"
|
||||
echo "SkyArtShop Backend Setup & Troubleshooting"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
|
||||
# 1. Generate password hash
|
||||
echo "Step 1: Generating password hash..."
|
||||
node -e "const bcrypt = require('bcrypt'); bcrypt.hash('Admin123!', 10).then(hash => console.log(hash));" > /tmp/pwhash.txt
|
||||
HASH=$(cat /tmp/pwhash.txt)
|
||||
echo "Generated hash: $HASH"
|
||||
echo ""
|
||||
|
||||
# 2. Setup database
|
||||
echo "Step 2: Setting up database..."
|
||||
PGPASSWORD=SkyArt2025Pass! psql -U skyartapp -d skyartshop <<EOF
|
||||
-- Create tables
|
||||
CREATE TABLE IF NOT EXISTS adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
passwordhash TEXT NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'admin',
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
lastlogin TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
sid VARCHAR NOT NULL COLLATE "default",
|
||||
sess JSON NOT NULL,
|
||||
expire TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (sid)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_session_expire ON session (expire);
|
||||
|
||||
-- Delete existing admin if present
|
||||
DELETE FROM adminusers WHERE email = 'admin@skyartshop.com';
|
||||
|
||||
-- Insert new admin with generated hash
|
||||
INSERT INTO adminusers (email, name, passwordhash, role, createdat)
|
||||
VALUES ('admin@skyartshop.com', 'Admin User', '$HASH', 'superadmin', NOW());
|
||||
|
||||
-- Show result
|
||||
SELECT id, email, name, role FROM adminusers;
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Checking if backend is running..."
|
||||
if pgrep -f "node.*server.js" > /dev/null; then
|
||||
echo "✓ Backend is running"
|
||||
echo " PID: $(pgrep -f 'node.*server.js')"
|
||||
else
|
||||
echo "✗ Backend is NOT running"
|
||||
echo ""
|
||||
echo "Starting backend server..."
|
||||
cd /var/www/SkyArtShop/backend
|
||||
nohup node server.js > /tmp/skyartshop-backend.log 2>&1 &
|
||||
sleep 2
|
||||
|
||||
if pgrep -f "node.*server.js" > /dev/null; then
|
||||
echo "✓ Backend started successfully"
|
||||
else
|
||||
echo "✗ Failed to start backend. Check logs:"
|
||||
cat /tmp/skyartshop-backend.log
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Checking port 3001..."
|
||||
if netstat -tln 2>/dev/null | grep -q ":3001 " || ss -tln 2>/dev/null | grep -q ":3001 "; then
|
||||
echo "✓ Port 3001 is listening"
|
||||
else
|
||||
echo "✗ Port 3001 is NOT listening"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================================"
|
||||
echo "LOGIN CREDENTIALS"
|
||||
echo "================================================"
|
||||
echo "URL: http://localhost:3001/admin/login"
|
||||
echo " or http://your-domain.com/admin/login"
|
||||
echo ""
|
||||
echo "Email: admin@skyartshop.com"
|
||||
echo "Password: Admin123!"
|
||||
echo "================================================"
|
||||
echo ""
|
||||
echo "If still having issues, check logs:"
|
||||
echo " tail -f /tmp/skyartshop-backend.log"
|
||||
echo "================================================"
|
||||
@@ -1,270 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Create auth routes
|
||||
cat > routes/auth.js << 'EOF'
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { query } = require('../config/database');
|
||||
const { redirectIfAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/login', redirectIfAuth, (req, res) => {
|
||||
res.render('admin/login', {
|
||||
error: req.query.error,
|
||||
title: 'Admin Login - SkyArtShop'
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT id, email, name, password, role FROM adminusers WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.redirect('/admin/login?error=invalid');
|
||||
}
|
||||
const admin = result.rows[0];
|
||||
const validPassword = await bcrypt.compare(password, admin.password);
|
||||
if (!validPassword) {
|
||||
return res.redirect('/admin/login?error=invalid');
|
||||
}
|
||||
await query('UPDATE adminusers SET lastlogin = NOW() WHERE id = $1', [admin.id]);
|
||||
req.session.adminId = admin.id;
|
||||
req.session.email = admin.email;
|
||||
req.session.name = admin.name;
|
||||
req.session.role = admin.role;
|
||||
res.redirect('/admin/dashboard');
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.redirect('/admin/login?error=server');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) console.error('Logout error:', err);
|
||||
res.redirect('/admin/login');
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
EOF
|
||||
|
||||
# Create admin routes
|
||||
cat > routes/admin.js << 'EOF'
|
||||
const express = require('express');
|
||||
const { query } = require('../config/database');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/dashboard', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const productsCount = await query('SELECT COUNT(*) FROM products');
|
||||
const ordersCount = await query('SELECT COUNT(*) FROM orders');
|
||||
const usersCount = await query('SELECT COUNT(*) FROM appusers');
|
||||
const pagesCount = await query('SELECT COUNT(*) FROM pages');
|
||||
const recentOrders = await query(
|
||||
'SELECT id, ordernumber, totalamount, status, createdat FROM orders ORDER BY createdat DESC LIMIT 5'
|
||||
);
|
||||
res.render('admin/dashboard', {
|
||||
title: 'Dashboard - SkyArtShop Admin',
|
||||
user: req.session,
|
||||
stats: {
|
||||
products: productsCount.rows[0].count,
|
||||
orders: ordersCount.rows[0].count,
|
||||
users: usersCount.rows[0].count,
|
||||
pages: pagesCount.rows[0].count
|
||||
},
|
||||
recentOrders: recentOrders.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/products', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT id, name, price, stockquantity, isactive, createdat FROM products ORDER BY createdat DESC'
|
||||
);
|
||||
res.render('admin/products', {
|
||||
title: 'Products - SkyArtShop Admin',
|
||||
user: req.session,
|
||||
products: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Products error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/orders', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT id, ordernumber, totalamount, status, createdat FROM orders ORDER BY createdat DESC'
|
||||
);
|
||||
res.render('admin/orders', {
|
||||
title: 'Orders - SkyArtShop Admin',
|
||||
user: req.session,
|
||||
orders: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Orders error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/users', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
'SELECT id, email, name, role, createdat, lastlogin FROM adminusers ORDER BY createdat DESC'
|
||||
);
|
||||
res.render('admin/users', {
|
||||
title: 'Admin Users - SkyArtShop Admin',
|
||||
user: req.session,
|
||||
users: result.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Users error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
EOF
|
||||
|
||||
# Create public routes
|
||||
cat > routes/public.js << 'EOF'
|
||||
const express = require('express');
|
||||
const { query } = require('../config/database');
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const products = await query(
|
||||
'SELECT id, name, description, price, imageurl FROM products WHERE isactive = true ORDER BY createdat DESC LIMIT 8'
|
||||
);
|
||||
const sections = await query(
|
||||
'SELECT * FROM homepagesections ORDER BY displayorder ASC'
|
||||
);
|
||||
res.render('public/home', {
|
||||
title: 'Welcome - SkyArtShop',
|
||||
products: products.rows,
|
||||
sections: sections.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Home page error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/shop', async (req, res) => {
|
||||
try {
|
||||
const products = await query(
|
||||
'SELECT id, name, description, price, imageurl, category FROM products WHERE isactive = true ORDER BY name ASC'
|
||||
);
|
||||
res.render('public/shop', {
|
||||
title: 'Shop - SkyArtShop',
|
||||
products: products.rows
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Shop page error:', error);
|
||||
res.status(500).send('Server error');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
EOF
|
||||
|
||||
# Create main server.js
|
||||
cat > server.js << 'EOF'
|
||||
const express = require('express');
|
||||
const session = require('express-session');
|
||||
const pgSession = require('connect-pg-simple')(session);
|
||||
const path = require('path');
|
||||
const { pool } = require('./config/database');
|
||||
require('dotenv').config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
app.use('/assets', express.static(path.join(__dirname, '../wwwroot/assets')));
|
||||
app.use('/uploads', express.static(path.join(__dirname, '../wwwroot/uploads')));
|
||||
|
||||
app.use(session({
|
||||
store: new pgSession({
|
||||
pool: pool,
|
||||
tableName: 'session',
|
||||
createTableIfMissing: true
|
||||
}),
|
||||
secret: process.env.SESSION_SECRET || 'skyart-shop-secret-2025',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false,
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000
|
||||
}
|
||||
}));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
res.locals.session = req.session;
|
||||
res.locals.currentPath = req.path;
|
||||
next();
|
||||
});
|
||||
|
||||
const authRoutes = require('./routes/auth');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
const publicRoutes = require('./routes/public');
|
||||
|
||||
app.use('/admin', authRoutes);
|
||||
app.use('/admin', adminRoutes);
|
||||
app.use('/', publicRoutes);
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
database: 'connected'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('public/404', {
|
||||
title: '404 - Page Not Found'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
res.status(500).send('Server error');
|
||||
});
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log('========================================');
|
||||
console.log(' SkyArtShop Backend Server');
|
||||
console.log('========================================');
|
||||
console.log(`🚀 Server running on http://localhost:${PORT}`);
|
||||
console.log(`📦 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`🗄️ Database: PostgreSQL (${process.env.DB_NAME})`);
|
||||
console.log('========================================');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, closing server...');
|
||||
pool.end(() => {
|
||||
console.log('Database pool closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
EOF
|
||||
|
||||
echo "✓ Server files created"
|
||||
@@ -1,70 +0,0 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
const { query } = require("./config/database");
|
||||
|
||||
async function createTempAdmin() {
|
||||
try {
|
||||
// Temporary credentials
|
||||
const email = "admin@skyartshop.com";
|
||||
const password = "TempAdmin2024!";
|
||||
const name = "Temporary Admin";
|
||||
const role = "superadmin";
|
||||
|
||||
// Hash the password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Check if user already exists
|
||||
const existing = await query("SELECT id FROM adminusers WHERE email = $1", [
|
||||
email,
|
||||
]);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
console.log("⚠️ Admin user already exists. Updating password...");
|
||||
await query(
|
||||
"UPDATE adminusers SET passwordhash = $1, name = $2, role = $3 WHERE email = $4",
|
||||
[passwordHash, name, role, email]
|
||||
);
|
||||
console.log("✓ Password updated successfully!");
|
||||
} else {
|
||||
// Create the admin user
|
||||
await query(
|
||||
`INSERT INTO adminusers (email, name, passwordhash, role, createdat)
|
||||
VALUES ($1, $2, $3, $4, NOW())`,
|
||||
[email, name, passwordHash, role]
|
||||
);
|
||||
console.log("✓ Temporary admin user created successfully!");
|
||||
}
|
||||
|
||||
console.log("\n========================================");
|
||||
console.log("TEMPORARY ADMIN CREDENTIALS");
|
||||
console.log("========================================");
|
||||
console.log("Email: ", email);
|
||||
console.log("Password: ", password);
|
||||
console.log("========================================");
|
||||
console.log("\n⚠️ IMPORTANT: Change this password after first login!\n");
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error creating admin user:", error);
|
||||
|
||||
if (error.code === "42P01") {
|
||||
console.error('\n❌ Table "adminusers" does not exist.');
|
||||
console.error("Please create the database schema first.");
|
||||
console.log("\nRun this SQL to create the table:");
|
||||
console.log(`
|
||||
CREATE TABLE IF NOT EXISTS adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
passwordhash TEXT NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'admin',
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
lastlogin TIMESTAMP
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
createTempAdmin();
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Creating view files..."
|
||||
|
||||
# Admin login
|
||||
cat > views/admin/login.ejs << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center min-vh-100">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="fw-bold"><i class="bi bi-shop text-primary"></i> SkyArtShop</h2>
|
||||
<p class="text-muted">Admin Login</p>
|
||||
</div>
|
||||
<% if (error === 'invalid') { %>
|
||||
<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> Invalid email or password</div>
|
||||
<% } else if (error === 'server') { %>
|
||||
<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> Server error. Please try again.</div>
|
||||
<% } %>
|
||||
<form method="POST" action="/admin/login">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
||||
<input type="email" class="form-control" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg"><i class="bi bi-box-arrow-in-right"></i> Login</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted"><a href="/" class="text-decoration-none"><i class="bi bi-arrow-left"></i> Back to website</a></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">Default: admin@example.com / password</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "✓ Views created"
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo "🎉 FINAL SYSTEM TEST - SKYARTSHOP"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test 1: Backend Health
|
||||
echo "1️⃣ Backend Health Check:"
|
||||
HEALTH=$(curl -s http://localhost:5000/health)
|
||||
echo " $HEALTH"
|
||||
echo ""
|
||||
|
||||
# Test 2: HTTPS Certificate
|
||||
echo "2️⃣ HTTPS Configuration:"
|
||||
if ss -tln | grep -q ":443 "; then
|
||||
echo " ✅ Port 443 listening"
|
||||
else
|
||||
echo " ❌ Port 443 not listening"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: HTTP to HTTPS Redirect
|
||||
echo "3️⃣ HTTP → HTTPS Redirect:"
|
||||
HTTP_TEST=$(curl -s -o /dev/null -w "%{http_code}" http://skyarts.ddns.net)
|
||||
if [ "$HTTP_TEST" == "301" ]; then
|
||||
echo " ✅ Redirecting correctly (HTTP 301)"
|
||||
else
|
||||
echo " ⚠️ HTTP Status: $HTTP_TEST"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: Login Flow
|
||||
echo "4️⃣ Admin Login Test (HTTPS):"
|
||||
rm -f /tmp/final-login-test.txt
|
||||
LOGIN_RESPONSE=$(curl -k -s -c /tmp/final-login-test.txt -X POST https://skyarts.ddns.net/admin/login \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "email=admin@example.com&password=Admin123" \
|
||||
-w "%{http_code}")
|
||||
|
||||
if echo "$LOGIN_RESPONSE" | grep -q "302"; then
|
||||
echo " ✅ Login successful (302 redirect)"
|
||||
else
|
||||
echo " ❌ Login failed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: Dashboard Access
|
||||
echo "5️⃣ Dashboard Access:"
|
||||
DASHBOARD=$(curl -k -s -b /tmp/final-login-test.txt https://skyarts.ddns.net/admin/dashboard | grep -o "<title>.*</title>")
|
||||
if echo "$DASHBOARD" | grep -q "Dashboard"; then
|
||||
echo " ✅ Dashboard accessible"
|
||||
echo " $DASHBOARD"
|
||||
else
|
||||
echo " ❌ Dashboard not accessible"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 6: Public Homepage
|
||||
echo "6️⃣ Public Homepage:"
|
||||
HOMEPAGE=$(curl -k -s https://skyarts.ddns.net | grep -o "<title>.*</title>")
|
||||
echo " $HOMEPAGE"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "✅ ALL SYSTEMS OPERATIONAL"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "🔐 LOGIN INFORMATION:"
|
||||
echo " URL: https://skyarts.ddns.net/admin/login"
|
||||
echo " Email: admin@example.com"
|
||||
echo " Password: Admin123"
|
||||
echo ""
|
||||
echo "🌍 PUBLIC SITE:"
|
||||
echo " URL: https://skyarts.ddns.net"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "📝 NOTES:"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "✓ Backend running on port 5000"
|
||||
echo "✓ Nginx handling HTTPS on port 443"
|
||||
echo "✓ SSL certificates valid"
|
||||
echo "✓ Database connected"
|
||||
echo "✓ Session management working"
|
||||
echo "✓ HTTP redirects to HTTPS"
|
||||
echo ""
|
||||
echo "If you still see 'site can't be reached':"
|
||||
echo "1. Clear your browser cache"
|
||||
echo "2. Try incognito/private mode"
|
||||
echo "3. Try from a different device/network"
|
||||
echo "4. Check your local DNS cache:"
|
||||
echo " - Windows: ipconfig /flushdns"
|
||||
echo " - Mac: sudo dscacheutil -flushcache"
|
||||
echo " - Linux: sudo systemd-resolve --flush-caches"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
@@ -1,20 +0,0 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
async function generateHash() {
|
||||
const password = "Admin123!";
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
|
||||
console.log("Password:", password);
|
||||
console.log("Hash:", hash);
|
||||
console.log("\nSQL to insert user:");
|
||||
console.log(
|
||||
`INSERT INTO adminusers (email, name, passwordhash, role) VALUES ('admin@skyartshop.com', 'Admin User', '${hash}', 'superadmin') ON CONFLICT (email) DO UPDATE SET passwordhash = '${hash}';`
|
||||
);
|
||||
}
|
||||
|
||||
generateHash()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
const bcrypt = require("bcrypt");
|
||||
|
||||
const password = process.argv[2] || "admin123";
|
||||
|
||||
bcrypt.hash(password, 10, (err, hash) => {
|
||||
if (err) {
|
||||
console.error("Error:", err);
|
||||
return;
|
||||
}
|
||||
console.log("Password:", password);
|
||||
console.log("Hash:", hash);
|
||||
console.log("\nUse this SQL to update:");
|
||||
console.log(
|
||||
`UPDATE adminusers SET passwordhash = '${hash}' WHERE email = 'admin@example.com';`
|
||||
);
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo " SKYARTSHOP HTTPS CONFIGURATION STATUS"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
echo "✅ Server Configuration:"
|
||||
echo " - Backend: Running on port 5000"
|
||||
echo " - Nginx HTTPS: Listening on port 443"
|
||||
echo " - SSL Certificates: Valid"
|
||||
echo ""
|
||||
|
||||
echo "✅ Local Testing (Working):"
|
||||
echo " - http://localhost/admin/login ✓"
|
||||
echo " - https://localhost/admin/login ✓"
|
||||
echo ""
|
||||
|
||||
echo "🌐 Network Configuration:"
|
||||
echo " - Server Private IP: $(hostname -I | awk '{print $1}')"
|
||||
echo " - Public IP (DNS): $(nslookup skyarts.ddns.net 2>/dev/null | grep "Address:" | tail -1 | awk '{print $2}')"
|
||||
echo " - Domain: skyarts.ddns.net"
|
||||
echo ""
|
||||
|
||||
echo "🔥 Firewall Status:"
|
||||
sudo ufw status | grep -E "443|Status"
|
||||
echo ""
|
||||
|
||||
echo "🔌 Port Status:"
|
||||
ss -tlnp 2>/dev/null | grep -E ":(80|443|5000)" | awk '{print " "$1" "$4}'
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo " ACTION REQUIRED"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Your server is behind a router/NAT."
|
||||
echo "To make https://skyarts.ddns.net accessible:"
|
||||
echo ""
|
||||
echo "1. LOG INTO YOUR ROUTER"
|
||||
echo " IP: Check your router's IP (usually 192.168.10.1)"
|
||||
echo ""
|
||||
echo "2. SET UP PORT FORWARDING:"
|
||||
echo " External Port: 443"
|
||||
echo " Internal IP: 192.168.10.130"
|
||||
echo " Internal Port: 443"
|
||||
echo " Protocol: TCP"
|
||||
echo ""
|
||||
echo "3. ALSO FORWARD (if not already done):"
|
||||
echo " External Port: 80"
|
||||
echo " Internal IP: 192.168.10.130"
|
||||
echo " Internal Port: 80"
|
||||
echo " Protocol: TCP"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " TEST AFTER PORT FORWARDING"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Once port forwarding is configured:"
|
||||
echo ""
|
||||
echo "1. From your browser:"
|
||||
echo " https://skyarts.ddns.net"
|
||||
echo " https://skyarts.ddns.net/admin/login"
|
||||
echo ""
|
||||
echo "2. Login credentials:"
|
||||
echo " Email: admin@example.com"
|
||||
echo " Password: Admin123"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
@@ -1,32 +0,0 @@
|
||||
-- Quick setup script for SkyArtShop backend
|
||||
-- Run with: psql -U skyartapp -d skyartshop -f quick-setup.sql
|
||||
|
||||
\echo 'Creating adminusers table...'
|
||||
CREATE TABLE IF NOT EXISTS adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
passwordhash TEXT NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'admin',
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
lastlogin TIMESTAMP
|
||||
);
|
||||
|
||||
\echo 'Creating temporary admin user...'
|
||||
-- Email: admin@skyartshop.com
|
||||
-- Password: Admin123!
|
||||
DELETE FROM adminusers WHERE email = 'admin@skyartshop.com';
|
||||
INSERT INTO adminusers (email, name, passwordhash, role) VALUES
|
||||
('admin@skyartshop.com', 'Admin User', '$2b$10$vN9gE1VTxH3qH3qH3qH3qOqXZ5J8YqH3qH3qH3qH3qH3qH3qH3qH3u', 'superadmin');
|
||||
|
||||
\echo 'Verifying admin user...'
|
||||
SELECT id, email, name, role, createdat FROM adminusers;
|
||||
|
||||
\echo ''
|
||||
\echo '========================================='
|
||||
\echo 'Setup Complete!'
|
||||
\echo '========================================='
|
||||
\echo 'Login credentials:'
|
||||
\echo ' Email: admin@skyartshop.com'
|
||||
\echo ' Password: Admin123!'
|
||||
\echo '========================================='
|
||||
@@ -1,48 +0,0 @@
|
||||
-- Create adminusers table if it doesn't exist
|
||||
CREATE TABLE IF NOT EXISTS adminusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
passwordhash TEXT NOT NULL,
|
||||
role VARCHAR(50) DEFAULT 'admin',
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
lastlogin TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert temporary admin user
|
||||
-- Password: TempAdmin2024!
|
||||
-- Bcrypt hash generated with 10 salt rounds
|
||||
INSERT INTO adminusers (email, name, passwordhash, role, createdat)
|
||||
VALUES (
|
||||
'admin@skyartshop.com',
|
||||
'Temporary Admin',
|
||||
'$2b$10$YvK5rQE4nHjZH5tVFZ1lNu5iK7Jx/lMQXZvhGEg8sK1vF0N3wL5oG',
|
||||
'superadmin',
|
||||
NOW()
|
||||
)
|
||||
ON CONFLICT (email) DO UPDATE
|
||||
SET passwordhash = EXCLUDED.passwordhash,
|
||||
name = EXCLUDED.name,
|
||||
role = EXCLUDED.role;
|
||||
|
||||
-- Create appusers table for public users (if needed)
|
||||
CREATE TABLE IF NOT EXISTS appusers (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
passwordhash TEXT NOT NULL,
|
||||
createdat TIMESTAMP DEFAULT NOW(),
|
||||
lastlogin TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create sessions table for express-session
|
||||
CREATE TABLE IF NOT EXISTS session (
|
||||
sid VARCHAR NOT NULL COLLATE "default",
|
||||
sess JSON NOT NULL,
|
||||
expire TIMESTAMP(6) NOT NULL,
|
||||
PRIMARY KEY (sid)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_session_expire ON session (expire);
|
||||
|
||||
SELECT 'Database setup complete!' as status;
|
||||
@@ -1,71 +0,0 @@
|
||||
#!/bin/bash
|
||||
echo "Creating backend files..."
|
||||
|
||||
# Database config
|
||||
cat > config/database.js << 'EOF'
|
||||
const { Pool } = require('pg');
|
||||
require('dotenv').config();
|
||||
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'skyartshop',
|
||||
user: process.env.DB_USER || 'skyartapp',
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
pool.on('connect', () => console.log('✓ PostgreSQL connected'));
|
||||
pool.on('error', (err) => console.error('PostgreSQL error:', err));
|
||||
|
||||
const query = async (text, params) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const res = await pool.query(text, params);
|
||||
const duration = Date.now() - start;
|
||||
console.log('Executed query', { text, duration, rows: res.rowCount });
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Query error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { pool, query };
|
||||
EOF
|
||||
|
||||
# Auth middleware
|
||||
cat > middleware/auth.js << 'EOF'
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (req.session && req.session.adminId) {
|
||||
return next();
|
||||
}
|
||||
res.redirect('/admin/login');
|
||||
};
|
||||
|
||||
const requireRole = (allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
if (!req.session || !req.session.adminId) {
|
||||
return res.redirect('/admin/login');
|
||||
}
|
||||
const userRole = req.session.role || 'user';
|
||||
if (allowedRoles.includes(userRole)) {
|
||||
return next();
|
||||
}
|
||||
res.status(403).send('Access denied');
|
||||
};
|
||||
};
|
||||
|
||||
const redirectIfAuth = (req, res, next) => {
|
||||
if (req.session && req.session.adminId) {
|
||||
return res.redirect('/admin/dashboard');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = { requireAuth, requireRole, redirectIfAuth };
|
||||
EOF
|
||||
|
||||
echo "✓ Files created successfully"
|
||||
@@ -1,46 +0,0 @@
|
||||
-- Create roles table
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
permissions JSONB DEFAULT '{}',
|
||||
createdat TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Insert default roles
|
||||
INSERT INTO roles (id, name, description, permissions) VALUES
|
||||
('role-admin', 'Admin', 'Full system access and management', '{"manage_users": true, "manage_products": true, "manage_orders": true, "manage_content": true, "view_reports": true, "manage_settings": true}'),
|
||||
('role-accountant', 'Accountant', 'Financial and reporting access', '{"view_orders": true, "view_reports": true, "manage_products": false, "manage_users": false}'),
|
||||
('role-sales', 'Sales', 'Product and order management', '{"manage_products": true, "manage_orders": true, "view_reports": true, "manage_users": false}'),
|
||||
('role-cashier', 'Cashier', 'Basic order processing', '{"process_orders": true, "view_products": true, "manage_products": false, "manage_users": false}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- Update adminusers table to add role and password expiry fields
|
||||
ALTER TABLE adminusers
|
||||
ADD COLUMN IF NOT EXISTS role_id VARCHAR(50) DEFAULT 'role-admin',
|
||||
ADD COLUMN IF NOT EXISTS password_expires_at TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS password_never_expires BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS last_password_change TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS isactive BOOLEAN DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS last_login TIMESTAMP,
|
||||
ADD COLUMN IF NOT EXISTS created_by VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- Add foreign key constraint
|
||||
ALTER TABLE adminusers
|
||||
ADD CONSTRAINT fk_role
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||
ON DELETE SET NULL;
|
||||
|
||||
-- Update existing admin user
|
||||
UPDATE adminusers
|
||||
SET role_id = 'role-admin',
|
||||
password_never_expires = true,
|
||||
isactive = true
|
||||
WHERE email = 'admin@example.com';
|
||||
|
||||
-- Create index for better performance
|
||||
CREATE INDEX IF NOT EXISTS idx_adminusers_role ON adminusers(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_adminusers_email ON adminusers(email);
|
||||
|
||||
SELECT 'User roles setup complete' as status;
|
||||
@@ -1,52 +0,0 @@
|
||||
const express = require("express");
|
||||
const bcrypt = require("bcrypt");
|
||||
const { query } = require("./config/database");
|
||||
|
||||
async function testLogin() {
|
||||
const email = "admin@example.com";
|
||||
const password = "Admin123";
|
||||
|
||||
try {
|
||||
console.log("1. Querying database for user...");
|
||||
const result = await query(
|
||||
"SELECT id, email, name, passwordhash, role FROM adminusers WHERE email = $1",
|
||||
[email]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
console.log("❌ User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("2. User found:", result.rows[0].email);
|
||||
|
||||
const admin = result.rows[0];
|
||||
console.log("3. Comparing password...");
|
||||
const validPassword = await bcrypt.compare(password, admin.passwordhash);
|
||||
|
||||
if (!validPassword) {
|
||||
console.log("❌ Invalid password");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("4. ✓ Password valid!");
|
||||
|
||||
console.log("5. Updating last login...");
|
||||
await query("UPDATE adminusers SET lastlogin = NOW() WHERE id = $1", [
|
||||
admin.id,
|
||||
]);
|
||||
|
||||
console.log("6. ✓ Login successful!");
|
||||
console.log(" User ID:", admin.id);
|
||||
console.log(" Email:", admin.email);
|
||||
console.log(" Name:", admin.name);
|
||||
console.log(" Role:", admin.role);
|
||||
} catch (error) {
|
||||
console.error("❌ Error during login:", error.message);
|
||||
console.error(" Stack:", error.stack);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
testLogin();
|
||||
478
backend/package-lock.json
generated
478
backend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
@@ -23,6 +24,11 @@
|
||||
"pg": "^8.11.3",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^5.7.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"prisma": "^5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@colors/colors": {
|
||||
@@ -65,6 +71,75 @@
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.1.tgz",
|
||||
"integrity": "sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prisma": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"prisma": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz",
|
||||
"integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz",
|
||||
"integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/fetch-engine": "5.7.1",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz",
|
||||
"integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz",
|
||||
"integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz",
|
||||
"integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@so-ric/colorspace": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
|
||||
@@ -144,6 +219,20 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
@@ -216,6 +305,19 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
@@ -249,6 +351,19 @@
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -304,6 +419,31 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
@@ -368,6 +508,45 @@
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/compressible": {
|
||||
"version": "2.0.18",
|
||||
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": ">= 1.43.0 < 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/compression": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"compressible": "~2.0.18",
|
||||
"debug": "2.6.9",
|
||||
"negotiator": "~0.6.4",
|
||||
"on-headers": "~1.1.0",
|
||||
"safe-buffer": "5.2.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/compression/node_modules/negotiator": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -749,6 +928,19 @@
|
||||
"minimatch": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
@@ -821,6 +1013,21 @@
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -909,6 +1116,19 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
@@ -943,6 +1163,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@@ -1050,6 +1280,13 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@@ -1085,6 +1322,29 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
@@ -1094,6 +1354,29 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
@@ -1384,6 +1667,84 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||
"integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^3.5.2",
|
||||
"debug": "^4",
|
||||
"ignore-by-default": "^1.0.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"semver": "^7.5.3",
|
||||
"simple-update-notifier": "^2.0.0",
|
||||
"supports-color": "^5.5.0",
|
||||
"touch": "^3.1.0",
|
||||
"undefsafe": "^2.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"nodemon": "bin/nodemon.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -1399,6 +1760,16 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
@@ -1592,6 +1963,19 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
@@ -1631,6 +2015,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz",
|
||||
"integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.7.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
@@ -1650,6 +2052,13 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
@@ -1719,6 +2128,19 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
@@ -2006,6 +2428,19 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
@@ -2082,6 +2517,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
@@ -2117,6 +2565,19 @@
|
||||
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
@@ -2126,6 +2587,16 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -2172,6 +2643,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/undefsafe": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
@@ -23,5 +24,10 @@
|
||||
"pg": "^8.11.3",
|
||||
"uuid": "^9.0.1",
|
||||
"winston": "^3.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^5.7.1",
|
||||
"nodemon": "^3.1.11",
|
||||
"prisma": "^5.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
41
backend/prisma/schema.prisma
Normal file
41
backend/prisma/schema.prisma
Normal file
@@ -0,0 +1,41 @@
|
||||
// Prisma Schema
|
||||
// Database schema definition and ORM configuration
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = "postgresql://skyartapp:SkyArt2025Pass@localhost:5432/skyartshop?schema=public"
|
||||
}
|
||||
|
||||
// User model
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
role String @default("customer") // 'admin' or 'customer'
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
// Product model
|
||||
model Product {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String @db.Text
|
||||
price Decimal @db.Decimal(10, 2)
|
||||
category String
|
||||
stock Int @default(0)
|
||||
images String[] // Array of image URLs
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("products")
|
||||
}
|
||||
|
||||
// Add more models as needed
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Quick Test: Create Product via Backend API
|
||||
# Usage: ./quick-test-create-product.sh
|
||||
|
||||
API_BASE="http://localhost:5000/api"
|
||||
|
||||
echo "============================================"
|
||||
echo " Backend Product Creation Test"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# Step 1: Login
|
||||
echo "1. Logging in..."
|
||||
LOGIN_RESPONSE=$(curl -s -c /tmp/product_test_cookies.txt -X POST "$API_BASE/admin/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@example.com",
|
||||
"password": "admin123"
|
||||
}')
|
||||
|
||||
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
|
||||
echo " ✅ Login successful"
|
||||
else
|
||||
echo " ❌ Login failed"
|
||||
echo "$LOGIN_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 2: Create Product
|
||||
echo ""
|
||||
echo "2. Creating product with color variants..."
|
||||
CREATE_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt -X POST "$API_BASE/admin/products" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Artistic Canvas Print",
|
||||
"shortdescription": "Beautiful handcrafted canvas art",
|
||||
"description": "<h3>Premium Canvas Art</h3><p>This stunning piece features:</p><ul><li>High-quality canvas</li><li>Vibrant colors</li><li>Ready to hang</li></ul>",
|
||||
"price": 149.99,
|
||||
"stockquantity": 20,
|
||||
"category": "Wall Art",
|
||||
"sku": "ART-CANVAS-001",
|
||||
"weight": 1.5,
|
||||
"dimensions": "20x30 inches",
|
||||
"material": "Canvas",
|
||||
"isactive": true,
|
||||
"isfeatured": true,
|
||||
"isbestseller": false,
|
||||
"images": [
|
||||
{
|
||||
"image_url": "/uploads/canvas-red.jpg",
|
||||
"color_variant": "Red",
|
||||
"alt_text": "Canvas Print - Red",
|
||||
"display_order": 0,
|
||||
"is_primary": true
|
||||
},
|
||||
{
|
||||
"image_url": "/uploads/canvas-blue.jpg",
|
||||
"color_variant": "Blue",
|
||||
"alt_text": "Canvas Print - Blue",
|
||||
"display_order": 1,
|
||||
"is_primary": false
|
||||
},
|
||||
{
|
||||
"image_url": "/uploads/canvas-green.jpg",
|
||||
"color_variant": "Green",
|
||||
"alt_text": "Canvas Print - Green",
|
||||
"display_order": 2,
|
||||
"is_primary": false
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
PRODUCT_ID=$(echo "$CREATE_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$PRODUCT_ID" ]; then
|
||||
echo " ✅ Product created successfully!"
|
||||
echo " Product ID: $PRODUCT_ID"
|
||||
echo ""
|
||||
echo "$CREATE_RESPONSE" | jq '{
|
||||
success: .success,
|
||||
product: {
|
||||
id: .product.id,
|
||||
name: .product.name,
|
||||
slug: .product.slug,
|
||||
price: .product.price,
|
||||
sku: .product.sku,
|
||||
category: .product.category,
|
||||
isactive: .product.isactive,
|
||||
isfeatured: .product.isfeatured,
|
||||
image_count: (.product.images | length),
|
||||
color_variants: [.product.images[].color_variant]
|
||||
}
|
||||
}'
|
||||
else
|
||||
echo " ❌ Product creation failed"
|
||||
echo "$CREATE_RESPONSE" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: Verify product was created
|
||||
echo ""
|
||||
echo "3. Fetching product details..."
|
||||
GET_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt "$API_BASE/admin/products/$PRODUCT_ID")
|
||||
|
||||
if echo "$GET_RESPONSE" | grep -q '"success":true'; then
|
||||
echo " ✅ Product retrieved successfully"
|
||||
IMAGES_COUNT=$(echo "$GET_RESPONSE" | grep -o '"color_variant"' | wc -l)
|
||||
echo " Images with color variants: $IMAGES_COUNT"
|
||||
else
|
||||
echo " ❌ Failed to retrieve product"
|
||||
fi
|
||||
|
||||
# Step 4: List all products
|
||||
echo ""
|
||||
echo "4. Listing all products..."
|
||||
LIST_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt "$API_BASE/admin/products")
|
||||
TOTAL_PRODUCTS=$(echo "$LIST_RESPONSE" | grep -o '"id"' | wc -l)
|
||||
echo " ✅ Total products in system: $TOTAL_PRODUCTS"
|
||||
|
||||
# Step 5: Cleanup option
|
||||
echo ""
|
||||
read -p "Delete test product? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
DELETE_RESPONSE=$(curl -s -b /tmp/product_test_cookies.txt -X DELETE "$API_BASE/admin/products/$PRODUCT_ID")
|
||||
if echo "$DELETE_RESPONSE" | grep -q '"success":true'; then
|
||||
echo " ✅ Test product deleted"
|
||||
else
|
||||
echo " ❌ Failed to delete product"
|
||||
fi
|
||||
else
|
||||
echo " ℹ️ Test product kept: $PRODUCT_ID"
|
||||
echo " You can view it in the admin panel or delete manually"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /tmp/product_test_cookies.txt
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Test Complete!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "Backend API Endpoints Working:"
|
||||
echo " ✅ POST /api/admin/products - Create product"
|
||||
echo " ✅ GET /api/admin/products/:id - Get product"
|
||||
echo " ✅ GET /api/admin/products - List all products"
|
||||
echo " ✅ PUT /api/admin/products/:id - Update product"
|
||||
echo " ✅ DELETE /api/admin/products/:id - Delete product"
|
||||
echo ""
|
||||
echo "Features Confirmed:"
|
||||
echo " ✅ Color variant support"
|
||||
echo " ✅ Multiple images per product"
|
||||
echo " ✅ Rich text HTML description"
|
||||
echo " ✅ All metadata fields (SKU, weight, dimensions, etc.)"
|
||||
echo " ✅ Active/Featured/Bestseller flags"
|
||||
echo ""
|
||||
109
backend/readme.md
Normal file
109
backend/readme.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# SkyArtShop Backend
|
||||
|
||||
Production-ready Node.js + Express + TypeScript backend for Sky Art Shop.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Set up database
|
||||
npx prisma generate
|
||||
npx prisma migrate dev
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Run production server
|
||||
npm start
|
||||
```
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── prisma/
|
||||
│ └── schema.prisma # Database schema
|
||||
├── src/
|
||||
│ ├── @types/ # TypeScript definitions
|
||||
│ ├── config/ # Configuration files
|
||||
│ ├── controllers/ # Request handlers
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── models/ # Data access layer
|
||||
│ ├── routes/ # API route definitions
|
||||
│ ├── middlewares/ # Express middleware
|
||||
│ ├── validators/ # Request validation
|
||||
│ ├── helpers/ # Utility functions
|
||||
│ └── server.ts # Entry point
|
||||
├── .env
|
||||
├── tsconfig.json
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Node.js** - Runtime
|
||||
- **Express** - Web framework
|
||||
- **TypeScript** - Type safety
|
||||
- **Prisma** - ORM
|
||||
- **PostgreSQL** - Database
|
||||
- **JWT** - Authentication
|
||||
- **Zod** - Validation
|
||||
|
||||
## 🔑 Environment Variables
|
||||
|
||||
Create a `.env` file:
|
||||
|
||||
```
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/skyartshop"
|
||||
JWT_SECRET=your-secret-key
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
```
|
||||
|
||||
## 📝 Development Guidelines
|
||||
|
||||
### Folder Responsibilities
|
||||
|
||||
- **controllers/**: Handle HTTP requests and responses
|
||||
- **services/**: Business logic and orchestration
|
||||
- **models/**: Database queries (Prisma models)
|
||||
- **routes/**: Define endpoints and apply middleware
|
||||
- **middlewares/**: Authentication, validation, logging
|
||||
- **validators/**: Zod schemas for request validation
|
||||
- **helpers/**: Pure utility functions
|
||||
|
||||
### Adding a New Feature
|
||||
|
||||
1. Create model in `prisma/schema.prisma`
|
||||
2. Run `npx prisma migrate dev`
|
||||
3. Create service in `src/services/`
|
||||
4. Create controller in `src/controllers/`
|
||||
5. Add routes in `src/routes/`
|
||||
6. Add validators in `src/validators/`
|
||||
|
||||
## 🔒 Security
|
||||
|
||||
- JWT authentication on protected routes
|
||||
- Input validation with Zod
|
||||
- Helmet for security headers
|
||||
- CORS configured
|
||||
- Rate limiting ready
|
||||
|
||||
## 📊 Database
|
||||
|
||||
```bash
|
||||
# Generate Prisma Client
|
||||
npx prisma generate
|
||||
|
||||
# Create migration
|
||||
npx prisma migrate dev --name description
|
||||
|
||||
# Open Prisma Studio
|
||||
npx prisma studio
|
||||
```
|
||||
@@ -10,7 +10,7 @@ const organizedContactHTML = `
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 24px; margin-bottom: 48px;">
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-bottom: 48px;">
|
||||
<!-- Phone Card -->
|
||||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: 16px; text-align: center; color: white; box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);">
|
||||
<div style="font-size: 48px; margin-bottom: 16px;">
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
const express = require("express");
|
||||
const { query } = require("../config/database");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { cache } = require("../middleware/cache");
|
||||
const {
|
||||
invalidateProductCache,
|
||||
invalidateBlogCache,
|
||||
invalidatePortfolioCache,
|
||||
invalidateHomepageCache,
|
||||
} = require("../utils/cacheInvalidation");
|
||||
const logger = require("../config/logger");
|
||||
const { asyncHandler } = require("../middleware/errorHandler");
|
||||
const {
|
||||
@@ -252,6 +259,10 @@ router.put(
|
||||
"/products/:id",
|
||||
requireAuth,
|
||||
asyncHandler(async (req, res) => {
|
||||
console.log("=== UPDATE PRODUCT API CALLED ===");
|
||||
console.log("Product ID:", req.params.id);
|
||||
console.log("Request body:", JSON.stringify(req.body, null, 2));
|
||||
|
||||
const {
|
||||
name,
|
||||
shortdescription,
|
||||
@@ -269,6 +280,8 @@ router.put(
|
||||
images,
|
||||
} = req.body;
|
||||
|
||||
console.log("Images to save:", images);
|
||||
|
||||
// Generate slug if name is provided
|
||||
const slug = name ? generateSlug(name) : null;
|
||||
|
||||
@@ -344,16 +357,27 @@ router.put(
|
||||
return sendNotFound(res, "Product");
|
||||
}
|
||||
|
||||
console.log("Product updated in database:", result.rows[0].id);
|
||||
|
||||
// Update images if provided
|
||||
if (images && Array.isArray(images)) {
|
||||
console.log("Updating images, count:", images.length);
|
||||
|
||||
// Delete existing images for this product
|
||||
await query("DELETE FROM product_images WHERE product_id = $1", [
|
||||
req.params.id,
|
||||
]);
|
||||
const deleteResult = await query(
|
||||
"DELETE FROM product_images WHERE product_id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
console.log("Deleted existing images, count:", deleteResult.rowCount);
|
||||
|
||||
// Insert new images
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const img = images[i];
|
||||
console.log(
|
||||
`Inserting image ${i + 1}/${images.length}:`,
|
||||
img.image_url
|
||||
);
|
||||
|
||||
await query(
|
||||
`INSERT INTO product_images (
|
||||
product_id, image_url, color_variant, color_code, alt_text, display_order, is_primary, variant_price, variant_stock
|
||||
@@ -371,6 +395,9 @@ router.put(
|
||||
]
|
||||
);
|
||||
}
|
||||
console.log("All images inserted successfully");
|
||||
} else {
|
||||
console.log("No images to update");
|
||||
}
|
||||
|
||||
// Fetch complete product with images
|
||||
@@ -396,6 +423,12 @@ router.put(
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
console.log("Final product with images:", completeProduct.rows[0]);
|
||||
console.log("=== PRODUCT UPDATE COMPLETE ===");
|
||||
|
||||
// Invalidate product cache
|
||||
invalidateProductCache();
|
||||
|
||||
sendSuccess(res, {
|
||||
product: completeProduct.rows[0],
|
||||
message: "Product updated successfully",
|
||||
@@ -655,10 +688,15 @@ router.post(
|
||||
ispublished,
|
||||
pagedata,
|
||||
} = req.body;
|
||||
|
||||
// Generate readable ID from slug
|
||||
const pageId = `page-${slug}`;
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO pages (title, slug, content, pagecontent, metatitle, metadescription, ispublished, isactive, pagedata, createdat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) RETURNING *`,
|
||||
`INSERT INTO pages (id, title, slug, content, pagecontent, metatitle, metadescription, ispublished, isactive, pagedata, createdat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW()) RETURNING *`,
|
||||
[
|
||||
pageId,
|
||||
title,
|
||||
slug,
|
||||
content,
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
const express = require("express");
|
||||
const { query } = require("../config/database");
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const logger = require("../config/logger");
|
||||
const { asyncHandler } = require("../middleware/errorHandler");
|
||||
const router = express.Router();
|
||||
|
||||
// Dashboard stats API
|
||||
router.get("/dashboard/stats", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const productsCount = await query("SELECT COUNT(*) FROM products");
|
||||
const projectsCount = await query("SELECT COUNT(*) FROM portfolioprojects");
|
||||
const blogCount = await query("SELECT COUNT(*) FROM blogposts");
|
||||
const pagesCount = await query("SELECT COUNT(*) FROM pages");
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
stats: {
|
||||
products: parseInt(productsCount.rows[0].count),
|
||||
projects: parseInt(projectsCount.rows[0].count),
|
||||
blog: parseInt(blogCount.rows[0].count),
|
||||
pages: parseInt(pagesCount.rows[0].count),
|
||||
},
|
||||
user: {
|
||||
name: req.session.name,
|
||||
email: req.session.email,
|
||||
role: req.session.role,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Dashboard error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Products API
|
||||
router.get("/products", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, name, price, stockquantity, isactive, createdat FROM products ORDER BY createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
products: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Products error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio Projects API
|
||||
router.get("/portfolio/projects", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, title, description, imageurl, categoryid, createdat FROM portfolioprojects ORDER BY createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
projects: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Portfolio error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Blog Posts API
|
||||
router.get("/blog", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, title, slug, excerpt, ispublished, createdat FROM blogposts ORDER BY createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
posts: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Blog error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Pages API
|
||||
router.get("/pages", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, title, slug, ispublished, createdat FROM pages ORDER BY createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
pages: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Pages error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single product
|
||||
router.get("/products/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT * FROM products WHERE id = $1", [
|
||||
req.params.id,
|
||||
]);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Product not found" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
product: result.rows[0],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Product error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create product
|
||||
router.post("/products", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
stockquantity,
|
||||
category,
|
||||
isactive,
|
||||
isbestseller,
|
||||
} = req.body;
|
||||
|
||||
const result = await query(
|
||||
`INSERT INTO products (name, description, price, stockquantity, category, isactive, isbestseller, createdat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
stockquantity || 0,
|
||||
category,
|
||||
isactive !== false,
|
||||
isbestseller || false,
|
||||
]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
product: result.rows[0],
|
||||
message: "Product created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Create product error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Update product
|
||||
router.put("/products/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
stockquantity,
|
||||
category,
|
||||
isactive,
|
||||
isbestseller,
|
||||
} = req.body;
|
||||
|
||||
const result = await query(
|
||||
`UPDATE products
|
||||
SET name = $1, description = $2, price = $3, stockquantity = $4,
|
||||
category = $5, isactive = $6, isbestseller = $7, updatedat = NOW()
|
||||
WHERE id = $8
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
description,
|
||||
price,
|
||||
stockquantity || 0,
|
||||
category,
|
||||
isactive !== false,
|
||||
isbestseller || false,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Product not found" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
product: result.rows[0],
|
||||
message: "Product updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Update product error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete product
|
||||
router.delete("/products/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"DELETE FROM products WHERE id = $1 RETURNING id",
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Product not found" });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Product deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Delete product error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio Project CRUD
|
||||
router.get("/portfolio/projects/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT * FROM portfolioprojects WHERE id = $1",
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Project not found" });
|
||||
}
|
||||
res.json({ success: true, project: result.rows[0] });
|
||||
} catch (error) {
|
||||
logger.error("Portfolio project error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/portfolio/projects", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, description, category, isactive } = req.body;
|
||||
const result = await query(
|
||||
`INSERT INTO portfolioprojects (title, description, category, isactive, createdat)
|
||||
VALUES ($1, $2, $3, $4, NOW()) RETURNING *`,
|
||||
[title, description, category, isactive !== false]
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
project: result.rows[0],
|
||||
message: "Project created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Create portfolio project error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/portfolio/projects/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, description, category, isactive } = req.body;
|
||||
const result = await query(
|
||||
`UPDATE portfolioprojects
|
||||
SET title = $1, description = $2, category = $3, isactive = $4, updatedat = NOW()
|
||||
WHERE id = $5 RETURNING *`,
|
||||
[title, description, category, isactive !== false, req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Project not found" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
project: result.rows[0],
|
||||
message: "Project updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Update portfolio project error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/portfolio/projects/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"DELETE FROM portfolioprojects WHERE id = $1 RETURNING id",
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Project not found" });
|
||||
}
|
||||
res.json({ success: true, message: "Project deleted successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Delete portfolio project error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Blog Post CRUD
|
||||
router.get("/blog/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT * FROM blogposts WHERE id = $1", [
|
||||
req.params.id,
|
||||
]);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Blog post not found" });
|
||||
}
|
||||
res.json({ success: true, post: result.rows[0] });
|
||||
} catch (error) {
|
||||
logger.error("Blog post error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/blog", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
metatitle,
|
||||
metadescription,
|
||||
ispublished,
|
||||
} = req.body;
|
||||
const result = await query(
|
||||
`INSERT INTO blogposts (title, slug, excerpt, content, metatitle, metadescription, ispublished, createdat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW()) RETURNING *`,
|
||||
[
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
metatitle,
|
||||
metadescription,
|
||||
ispublished || false,
|
||||
]
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
post: result.rows[0],
|
||||
message: "Blog post created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Create blog post error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/blog/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
metatitle,
|
||||
metadescription,
|
||||
ispublished,
|
||||
} = req.body;
|
||||
const result = await query(
|
||||
`UPDATE blogposts
|
||||
SET title = $1, slug = $2, excerpt = $3, content = $4, metatitle = $5,
|
||||
metadescription = $6, ispublished = $7, updatedat = NOW()
|
||||
WHERE id = $8 RETURNING *`,
|
||||
[
|
||||
title,
|
||||
slug,
|
||||
excerpt,
|
||||
content,
|
||||
metatitle,
|
||||
metadescription,
|
||||
ispublished || false,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Blog post not found" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
post: result.rows[0],
|
||||
message: "Blog post updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Update blog post error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/blog/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"DELETE FROM blogposts WHERE id = $1 RETURNING id",
|
||||
[req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Blog post not found" });
|
||||
}
|
||||
res.json({ success: true, message: "Blog post deleted successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Delete blog post error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Custom Pages CRUD
|
||||
router.get("/pages/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query("SELECT * FROM pages WHERE id = $1", [
|
||||
req.params.id,
|
||||
]);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Page not found" });
|
||||
}
|
||||
res.json({ success: true, page: result.rows[0] });
|
||||
} catch (error) {
|
||||
logger.error("Page error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/pages", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, slug, content, metatitle, metadescription, ispublished } =
|
||||
req.body;
|
||||
const result = await query(
|
||||
`INSERT INTO pages (title, slug, content, metatitle, metadescription, ispublished, createdat)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW()) RETURNING *`,
|
||||
[title, slug, content, metatitle, metadescription, ispublished !== false]
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
page: result.rows[0],
|
||||
message: "Page created successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Create page error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/pages/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { title, slug, content, metatitle, metadescription, ispublished } =
|
||||
req.body;
|
||||
const result = await query(
|
||||
`UPDATE pages
|
||||
SET title = $1, slug = $2, content = $3, metatitle = $4,
|
||||
metadescription = $5, ispublished = $6, updatedat = NOW()
|
||||
WHERE id = $7 RETURNING *`,
|
||||
[
|
||||
title,
|
||||
slug,
|
||||
content,
|
||||
metatitle,
|
||||
metadescription,
|
||||
ispublished !== false,
|
||||
req.params.id,
|
||||
]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Page not found" });
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
page: result.rows[0],
|
||||
message: "Page updated successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Update page error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/pages/:id", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query("DELETE FROM pages WHERE id = $1 RETURNING id", [
|
||||
req.params.id,
|
||||
]);
|
||||
if (result.rows.length === 0) {
|
||||
return res
|
||||
.status(404)
|
||||
.json({ success: false, message: "Page not found" });
|
||||
}
|
||||
res.json({ success: true, message: "Page deleted successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Delete page error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Homepage Settings
|
||||
router.get("/homepage/settings", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT settings FROM site_settings WHERE key = 'homepage'"
|
||||
);
|
||||
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
logger.error("Homepage settings error:", error);
|
||||
res.json({ success: true, settings: {} });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/homepage/settings", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = req.body;
|
||||
await query(
|
||||
`INSERT INTO site_settings (key, settings, updatedat)
|
||||
VALUES ('homepage', $1, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
|
||||
[JSON.stringify(settings)]
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Homepage settings saved successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Save homepage settings error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// General Settings
|
||||
router.get("/settings", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT settings FROM site_settings WHERE key = 'general'"
|
||||
);
|
||||
const settings = result.rows.length > 0 ? result.rows[0].settings : {};
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
logger.error("Settings error:", error);
|
||||
res.json({ success: true, settings: {} });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/settings", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = req.body;
|
||||
await query(
|
||||
`INSERT INTO site_settings (key, settings, updatedat)
|
||||
VALUES ('general', $1, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
|
||||
[JSON.stringify(settings)]
|
||||
);
|
||||
res.json({ success: true, message: "Settings saved successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Save settings error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Menu Management
|
||||
router.get("/menu", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT settings FROM site_settings WHERE key = 'menu'"
|
||||
);
|
||||
const items =
|
||||
result.rows.length > 0 ? result.rows[0].settings.items || [] : [];
|
||||
res.json({ success: true, items });
|
||||
} catch (error) {
|
||||
logger.error("Menu error:", error);
|
||||
res.json({ success: true, items: [] });
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/menu", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
await query(
|
||||
`INSERT INTO site_settings (key, settings, updatedat)
|
||||
VALUES ('menu', $1, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET settings = $1, updatedat = NOW()`,
|
||||
[JSON.stringify({ items })]
|
||||
);
|
||||
res.json({ success: true, message: "Menu saved successfully" });
|
||||
} catch (error) {
|
||||
logger.error("Save menu error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,6 +2,7 @@ const express = require("express");
|
||||
const { query } = require("../config/database");
|
||||
const logger = require("../config/logger");
|
||||
const { asyncHandler } = require("../middleware/errorHandler");
|
||||
const { cacheMiddleware, cache } = require("../middleware/cache");
|
||||
const {
|
||||
sendSuccess,
|
||||
sendError,
|
||||
@@ -14,23 +15,30 @@ const handleDatabaseError = (res, error, context) => {
|
||||
sendError(res);
|
||||
};
|
||||
|
||||
// Get all products
|
||||
// Get all products - Cached for 5 minutes
|
||||
router.get(
|
||||
"/products",
|
||||
cacheMiddleware(300000), // 5 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await query(
|
||||
`SELECT p.id, p.name, p.slug, p.shortdescription, p.description, p.price,
|
||||
p.category, p.stockquantity, p.sku, p.weight, p.dimensions,
|
||||
p.material, p.isfeatured, p.isbestseller, p.createdat,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', pi.id,
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'alt_text', pi.alt_text,
|
||||
'is_primary', pi.is_primary
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL) as images
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'id', pi.id,
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'color_code', pi.color_code,
|
||||
'alt_text', pi.alt_text,
|
||||
'is_primary', pi.is_primary,
|
||||
'variant_price', pi.variant_price,
|
||||
'variant_stock', pi.variant_stock
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) as images
|
||||
FROM products p
|
||||
LEFT JOIN product_images pi ON pi.product_id = p.id
|
||||
WHERE p.isactive = true
|
||||
@@ -41,20 +49,27 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
// Get featured products
|
||||
// Get featured products - Cached for 10 minutes
|
||||
router.get(
|
||||
"/products/featured",
|
||||
cacheMiddleware(600000, (req) => `featured:${req.query.limit || 4}`), // 10 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const limit = parseInt(req.query.limit) || 4;
|
||||
const limit = Math.min(parseInt(req.query.limit) || 4, 20); // Max 20 items
|
||||
const result = await query(
|
||||
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price, p.category,
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'alt_text', pi.alt_text
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL) as images
|
||||
`SELECT p.id, p.name, p.slug, p.shortdescription, p.price, p.category, p.stockquantity,
|
||||
COALESCE(
|
||||
json_agg(
|
||||
json_build_object(
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'color_code', pi.color_code,
|
||||
'alt_text', pi.alt_text,
|
||||
'variant_price', pi.variant_price,
|
||||
'variant_stock', pi.variant_stock
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL),
|
||||
'[]'::json
|
||||
) as images
|
||||
FROM products p
|
||||
LEFT JOIN product_images pi ON pi.product_id = p.id
|
||||
WHERE p.isactive = true AND p.isfeatured = true
|
||||
@@ -89,9 +104,12 @@ router.get(
|
||||
'id', pi.id,
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'color_code', pi.color_code,
|
||||
'alt_text', pi.alt_text,
|
||||
'display_order', pi.display_order,
|
||||
'is_primary', pi.is_primary
|
||||
'is_primary', pi.is_primary,
|
||||
'variant_price', pi.variant_price,
|
||||
'variant_stock', pi.variant_stock
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL) as images
|
||||
FROM products p
|
||||
@@ -109,9 +127,12 @@ router.get(
|
||||
'id', pi.id,
|
||||
'image_url', pi.image_url,
|
||||
'color_variant', pi.color_variant,
|
||||
'color_code', pi.color_code,
|
||||
'alt_text', pi.alt_text,
|
||||
'display_order', pi.display_order,
|
||||
'is_primary', pi.is_primary
|
||||
'is_primary', pi.is_primary,
|
||||
'variant_price', pi.variant_price,
|
||||
'variant_stock', pi.variant_stock
|
||||
) ORDER BY pi.display_order, pi.created_at
|
||||
) FILTER (WHERE pi.id IS NOT NULL) as images
|
||||
FROM products p
|
||||
@@ -130,6 +151,21 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
// Get all product categories - Cached for 30 minutes
|
||||
router.get(
|
||||
"/categories",
|
||||
cacheMiddleware(1800000), // 30 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await query(
|
||||
`SELECT DISTINCT category
|
||||
FROM products
|
||||
WHERE isactive = true AND category IS NOT NULL AND category != ''
|
||||
ORDER BY category ASC`
|
||||
);
|
||||
sendSuccess(res, { categories: result.rows.map((row) => row.category) });
|
||||
})
|
||||
);
|
||||
|
||||
// Get site settings
|
||||
router.get(
|
||||
"/settings",
|
||||
@@ -139,9 +175,10 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
// Get homepage sections
|
||||
// Get homepage sections - Cached for 15 minutes
|
||||
router.get(
|
||||
"/homepage/sections",
|
||||
cacheMiddleware(900000), // 15 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await query(
|
||||
"SELECT * FROM homepagesections ORDER BY displayorder ASC"
|
||||
@@ -149,10 +186,10 @@ router.get(
|
||||
sendSuccess(res, { sections: result.rows });
|
||||
})
|
||||
);
|
||||
|
||||
// Get portfolio projects
|
||||
// Get portfolio projects - Cached for 10 minutes
|
||||
router.get(
|
||||
"/portfolio/projects",
|
||||
cacheMiddleware(600000), // 10 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await query(
|
||||
`SELECT id, title, description, featuredimage, images, category,
|
||||
@@ -164,9 +201,10 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
// Get blog posts
|
||||
// Get blog posts - Cached for 5 minutes
|
||||
router.get(
|
||||
"/blog/posts",
|
||||
cacheMiddleware(300000), // 5 minutes cache
|
||||
asyncHandler(async (req, res) => {
|
||||
const result = await query(
|
||||
`SELECT id, title, slug, excerpt, content, imageurl, ispublished, createdat
|
||||
|
||||
@@ -54,62 +54,96 @@ router.get("/roles", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get single user by ID
|
||||
router.get("/:id", async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const result = await query(
|
||||
`
|
||||
SELECT
|
||||
u.id, u.username, u.email, u.name, u.role, u.isactive,
|
||||
u.last_login, u.createdat, u.passwordneverexpires, u.role_id
|
||||
FROM adminusers u
|
||||
WHERE u.id = $1
|
||||
`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: result.rows[0],
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Get user error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new user
|
||||
router.post("/", async (req, res) => {
|
||||
try {
|
||||
const { username, email, password, role_id, password_never_expires } =
|
||||
const { name, username, email, password, role, passwordneverexpires } =
|
||||
req.body;
|
||||
|
||||
// Validate required fields
|
||||
if (!username || !email || !password || !role_id) {
|
||||
if (!username || !email || !password || !role) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Username, email, password, and role are required",
|
||||
message: "Name, username, email, password, and role are required",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existing = await query("SELECT id FROM adminusers WHERE email = $1", [
|
||||
email,
|
||||
]);
|
||||
const existing = await query(
|
||||
"SELECT id FROM adminusers WHERE email = $1 OR username = $2",
|
||||
[email, username]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "User with this email already exists",
|
||||
message: "User with this email or username already exists",
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
// Hash password with bcrypt (10 rounds)
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Calculate password expiry (90 days from now if not never expires)
|
||||
let passwordExpiresAt = null;
|
||||
if (!password_never_expires) {
|
||||
if (!passwordneverexpires) {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + 90);
|
||||
passwordExpiresAt = expiryDate.toISOString();
|
||||
}
|
||||
|
||||
// Insert new user
|
||||
// Insert new user with both role and name fields
|
||||
const result = await query(
|
||||
`
|
||||
INSERT INTO adminusers (
|
||||
id, username, email, passwordhash, role_id,
|
||||
password_never_expires, password_expires_at,
|
||||
isactive, created_by, createdat, last_password_change
|
||||
id, name, username, email, passwordhash, role,
|
||||
passwordneverexpires, password_expires_at,
|
||||
isactive, created_by, createdat, lastpasswordchange
|
||||
) VALUES (
|
||||
'user-' || gen_random_uuid()::text,
|
||||
$1, $2, $3, $4, $5, $6, true, $7, NOW(), NOW()
|
||||
$1, $2, $3, $4, $5, $6, $7, true, $8, NOW(), NOW()
|
||||
)
|
||||
RETURNING id, username, email, role_id, isactive, createdat
|
||||
RETURNING id, name, username, email, role, isactive, createdat, passwordneverexpires
|
||||
`,
|
||||
[
|
||||
name || username,
|
||||
username,
|
||||
email,
|
||||
hashedPassword,
|
||||
role_id,
|
||||
password_never_expires || false,
|
||||
role,
|
||||
passwordneverexpires || false,
|
||||
passwordExpiresAt,
|
||||
req.session.user.email,
|
||||
]
|
||||
@@ -130,14 +164,25 @@ router.post("/", async (req, res) => {
|
||||
router.put("/:id", async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { username, email, role_id, isactive, password_never_expires } =
|
||||
req.body;
|
||||
const {
|
||||
name,
|
||||
username,
|
||||
email,
|
||||
role,
|
||||
isactive,
|
||||
passwordneverexpires,
|
||||
password,
|
||||
} = req.body;
|
||||
|
||||
// Build update query dynamically
|
||||
const updates = [];
|
||||
const values = [];
|
||||
let paramCount = 1;
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.push(`name = $${paramCount++}`);
|
||||
values.push(name);
|
||||
}
|
||||
if (username !== undefined) {
|
||||
updates.push(`username = $${paramCount++}`);
|
||||
values.push(username);
|
||||
@@ -146,25 +191,39 @@ router.put("/:id", async (req, res) => {
|
||||
updates.push(`email = $${paramCount++}`);
|
||||
values.push(email);
|
||||
}
|
||||
if (role_id !== undefined) {
|
||||
updates.push(`role_id = $${paramCount++}`);
|
||||
values.push(role_id);
|
||||
if (role !== undefined) {
|
||||
updates.push(`role = $${paramCount++}`);
|
||||
values.push(role);
|
||||
}
|
||||
if (isactive !== undefined) {
|
||||
updates.push(`isactive = $${paramCount++}`);
|
||||
values.push(isactive);
|
||||
}
|
||||
if (password_never_expires !== undefined) {
|
||||
updates.push(`password_never_expires = $${paramCount++}`);
|
||||
values.push(password_never_expires);
|
||||
if (passwordneverexpires !== undefined) {
|
||||
updates.push(`passwordneverexpires = $${paramCount++}`);
|
||||
values.push(passwordneverexpires);
|
||||
|
||||
// If setting to never expire, clear expiry date
|
||||
if (password_never_expires) {
|
||||
if (passwordneverexpires) {
|
||||
updates.push(`password_expires_at = NULL`);
|
||||
}
|
||||
}
|
||||
|
||||
updates.push(`updated_at = NOW()`);
|
||||
// Handle password update if provided
|
||||
if (password !== undefined && password !== "") {
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Password must be at least 8 characters long",
|
||||
});
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
updates.push(`passwordhash = $${paramCount++}`);
|
||||
values.push(hashedPassword);
|
||||
updates.push(`lastpasswordchange = NOW()`);
|
||||
}
|
||||
|
||||
updates.push(`updatedat = NOW()`);
|
||||
values.push(id);
|
||||
|
||||
const result = await query(
|
||||
@@ -172,7 +231,7 @@ router.put("/:id", async (req, res) => {
|
||||
UPDATE adminusers
|
||||
SET ${updates.join(", ")}
|
||||
WHERE id = $${paramCount}
|
||||
RETURNING id, username, email, role_id, isactive, password_never_expires
|
||||
RETURNING id, name, username, email, role, isactive, passwordneverexpires
|
||||
`,
|
||||
values
|
||||
);
|
||||
@@ -195,6 +254,66 @@ router.put("/:id", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Change user password (PUT endpoint for password modal)
|
||||
router.put("/:id/password", async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "Password must be at least 8 characters long",
|
||||
});
|
||||
}
|
||||
|
||||
// Hash new password with bcrypt (10 rounds)
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Get user's password expiry setting
|
||||
const userResult = await query(
|
||||
"SELECT passwordneverexpires FROM adminusers WHERE id = $1",
|
||||
[id]
|
||||
);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate new expiry date (90 days from now if not never expires)
|
||||
let passwordExpiresAt = null;
|
||||
if (!userResult.rows[0].passwordneverexpires) {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + 90);
|
||||
passwordExpiresAt = expiryDate.toISOString();
|
||||
}
|
||||
|
||||
// Update password
|
||||
await query(
|
||||
`
|
||||
UPDATE adminusers
|
||||
SET passwordhash = $1,
|
||||
password_expires_at = $2,
|
||||
lastpasswordchange = NOW(),
|
||||
updatedat = NOW()
|
||||
WHERE id = $3
|
||||
`,
|
||||
[hashedPassword, passwordExpiresAt, id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "Password changed successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Change password error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Reset user password
|
||||
router.post("/:id/reset-password", async (req, res) => {
|
||||
try {
|
||||
@@ -208,7 +327,7 @@ router.post("/:id/reset-password", async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
// Hash new password with bcrypt (10 rounds)
|
||||
const hashedPassword = await bcrypt.hash(new_password, 10);
|
||||
|
||||
// Get user's password expiry setting
|
||||
|
||||
@@ -5,6 +5,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const helmet = require("helmet");
|
||||
const cors = require("cors");
|
||||
const compressionMiddleware = require("./middleware/compression");
|
||||
const { pool, healthCheck } = require("./config/database");
|
||||
const logger = require("./config/logger");
|
||||
const { apiLimiter, authLimiter } = require("./config/rateLimiter");
|
||||
@@ -23,6 +24,13 @@ const baseDir = getBaseDir();
|
||||
|
||||
logger.info(`📁 Serving from: ${baseDir}`);
|
||||
|
||||
// Start cache cleanup scheduler
|
||||
const { startCleanup, stopCleanup } = require("./middleware/cache");
|
||||
startCleanup();
|
||||
|
||||
// Compression middleware - should be early in the chain
|
||||
app.use(compressionMiddleware);
|
||||
|
||||
// Security middleware
|
||||
app.use(
|
||||
helmet({
|
||||
@@ -107,9 +115,41 @@ const productImageFallback = (req, res, next) => {
|
||||
|
||||
app.use("/assets/images/products", productImageFallback);
|
||||
|
||||
app.use(express.static(path.join(baseDir, "public")));
|
||||
app.use("/assets", express.static(path.join(baseDir, "assets")));
|
||||
app.use("/uploads", express.static(path.join(baseDir, "uploads")));
|
||||
// Root redirect - serve the original HTML site
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(baseDir, "public", "home.html"));
|
||||
});
|
||||
|
||||
// Redirect /index to /home
|
||||
app.get("/index", (req, res) => {
|
||||
res.redirect("/home");
|
||||
});
|
||||
|
||||
app.use(
|
||||
express.static(path.join(baseDir, "public"), {
|
||||
index: false,
|
||||
maxAge: "1d", // Cache static files for 1 day
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
"/assets",
|
||||
express.static(path.join(baseDir, "assets"), {
|
||||
maxAge: "7d", // Cache assets for 7 days
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
immutable: true,
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
"/uploads",
|
||||
express.static(path.join(baseDir, "uploads"), {
|
||||
maxAge: "1d", // Cache uploads for 1 day
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Session middleware
|
||||
app.use(
|
||||
@@ -158,11 +198,52 @@ const uploadRoutes = require("./routes/upload");
|
||||
|
||||
// Admin redirect - handle /admin to redirect to login (must be before static files)
|
||||
app.get("/admin", (req, res) => {
|
||||
res.redirect("/admin/login.html");
|
||||
res.redirect("/admin/login");
|
||||
});
|
||||
|
||||
app.get("/admin/", (req, res) => {
|
||||
res.redirect("/admin/login.html");
|
||||
res.redirect("/admin/login");
|
||||
});
|
||||
|
||||
// URL Rewriting Middleware - Remove .html extension (must be before static files)
|
||||
app.use((req, res, next) => {
|
||||
// Skip API routes, static assets with extensions (except .html)
|
||||
if (
|
||||
req.path.startsWith("/api/") ||
|
||||
req.path.startsWith("/uploads/") ||
|
||||
req.path.startsWith("/assets/") ||
|
||||
(req.path.includes(".") && !req.path.endsWith(".html"))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if path is for admin area
|
||||
if (req.path.startsWith("/admin/")) {
|
||||
const cleanPath = req.path.replace(/\.html$/, "").replace(/^\/admin\//, "");
|
||||
const htmlPath = path.join(baseDir, "admin", cleanPath + ".html");
|
||||
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
return res.sendFile(htmlPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if path is for public pages (root level pages)
|
||||
if (!req.path.includes("/admin/")) {
|
||||
let cleanPath = req.path.replace(/^\//, "").replace(/\.html$/, "");
|
||||
|
||||
// Handle root path
|
||||
if (cleanPath === "" || cleanPath === "index") {
|
||||
cleanPath = "home";
|
||||
}
|
||||
|
||||
const htmlPath = path.join(baseDir, "public", cleanPath + ".html");
|
||||
|
||||
if (fs.existsSync(htmlPath)) {
|
||||
return res.sendFile(htmlPath);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Apply rate limiting to API routes
|
||||
@@ -177,16 +258,23 @@ app.use("/api/admin/users", usersRoutes);
|
||||
app.use("/api/admin", uploadRoutes);
|
||||
app.use("/api", publicRoutes);
|
||||
|
||||
// Admin static files (must be after redirect routes)
|
||||
app.use("/admin", express.static(path.join(baseDir, "admin")));
|
||||
// Admin static files (must be after URL rewriting)
|
||||
app.use(
|
||||
"/admin",
|
||||
express.static(path.join(baseDir, "admin"), {
|
||||
maxAge: "1d",
|
||||
etag: true,
|
||||
lastModified: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Favicon route
|
||||
app.get("/favicon.ico", (req, res) => {
|
||||
res.sendFile(path.join(baseDir, "public", "favicon.svg"));
|
||||
});
|
||||
|
||||
// Root redirect to home page
|
||||
app.get("/", (req, res) => {
|
||||
// Old site (if needed for reference)
|
||||
app.get("/old", (req, res) => {
|
||||
res.sendFile(path.join(baseDir, "public", "index.html"));
|
||||
});
|
||||
|
||||
@@ -248,6 +336,9 @@ const server = app.listen(PORT, "0.0.0.0", () => {
|
||||
const gracefulShutdown = (signal) => {
|
||||
logger.info(`${signal} received, shutting down gracefully...`);
|
||||
|
||||
// Stop cache cleanup
|
||||
stopCleanup();
|
||||
|
||||
server.close(() => {
|
||||
logger.info("HTTP server closed");
|
||||
|
||||
|
||||
48
backend/src/@types/index.ts
Normal file
48
backend/src/@types/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Shared TypeScript type definitions for backend
|
||||
*
|
||||
* Purpose: Centralized type definitions used across controllers, services, and models
|
||||
* Ensures type safety and consistency throughout the backend codebase
|
||||
*/
|
||||
|
||||
// User types
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
role: 'admin' | 'customer';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface AuthPayload {
|
||||
userId: number;
|
||||
username: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
// Product types
|
||||
export interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
category: string;
|
||||
stock: number;
|
||||
images: string[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// API Response types
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Request types (extends Express Request)
|
||||
export interface AuthRequest extends Request {
|
||||
user?: AuthPayload;
|
||||
}
|
||||
31
backend/src/config/app.ts
Normal file
31
backend/src/config/app.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Application Configuration
|
||||
*
|
||||
* Purpose: General application settings (port, CORS, JWT, rate limiting)
|
||||
* Loaded from environment variables with sensible defaults
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const appConfig = {
|
||||
// Server
|
||||
port: parseInt(process.env.PORT || '3000'),
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
|
||||
// JWT
|
||||
jwtSecret: process.env.JWT_SECRET || 'your-secret-key-change-in-production',
|
||||
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d',
|
||||
|
||||
// CORS
|
||||
corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||
|
||||
// Upload limits
|
||||
maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '5242880'), // 5MB default
|
||||
|
||||
// Rate limiting
|
||||
rateLimitWindow: 15 * 60 * 1000, // 15 minutes
|
||||
rateLimitMax: 100, // requests per window
|
||||
};
|
||||
|
||||
export default appConfig;
|
||||
31
backend/src/config/database.ts
Normal file
31
backend/src/config/database.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Database Configuration
|
||||
*
|
||||
* Purpose: Centralized database connection settings
|
||||
* Manages connection pooling, SSL, and environment-specific configs
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const databaseConfig = {
|
||||
// Database connection
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME || 'skyartshop',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
|
||||
// Connection pool settings
|
||||
pool: {
|
||||
min: 2,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
},
|
||||
|
||||
// SSL for production
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
};
|
||||
|
||||
export default databaseConfig;
|
||||
3
backend/src/controllers/.gitkeep
Normal file
3
backend/src/controllers/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# Controllers go here
|
||||
# Each controller handles HTTP requests for a specific resource
|
||||
# Example: productController.ts, authController.ts, userController.ts
|
||||
24
backend/src/helpers/jwt.ts
Normal file
24
backend/src/helpers/jwt.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* JWT Helper Functions
|
||||
*
|
||||
* Purpose: Generate and verify JWT tokens for authentication
|
||||
* Centralized token logic for consistency
|
||||
*/
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { appConfig } from '../config/app';
|
||||
import { AuthPayload } from '../@types';
|
||||
|
||||
export function generateToken(payload: AuthPayload): string {
|
||||
return jwt.sign(payload, appConfig.jwtSecret, {
|
||||
expiresIn: appConfig.jwtExpiresIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): AuthPayload | null {
|
||||
try {
|
||||
return jwt.verify(token, appConfig.jwtSecret) as AuthPayload;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
34
backend/src/helpers/response.ts
Normal file
34
backend/src/helpers/response.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Response Helper Functions
|
||||
*
|
||||
* Purpose: Consistent API response formatting across all endpoints
|
||||
* Ensures all responses follow the same structure
|
||||
*/
|
||||
|
||||
import { Response } from 'express';
|
||||
import { ApiResponse } from '../@types';
|
||||
|
||||
export function sendSuccess<T>(res: Response, data: T, message?: string, statusCode = 200): void {
|
||||
const response: ApiResponse<T> = {
|
||||
success: true,
|
||||
data,
|
||||
...(message && { message }),
|
||||
};
|
||||
res.status(statusCode).json(response);
|
||||
}
|
||||
|
||||
export function sendError(res: Response, error: string, statusCode = 400): void {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
res.status(statusCode).json(response);
|
||||
}
|
||||
|
||||
export function sendCreated<T>(res: Response, data: T, message = 'Resource created successfully'): void {
|
||||
sendSuccess(res, data, message, 201);
|
||||
}
|
||||
|
||||
export function sendNoContent(res: Response): void {
|
||||
res.status(204).send();
|
||||
}
|
||||
49
backend/src/middlewares/authenticate.ts
Normal file
49
backend/src/middlewares/authenticate.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Authentication Middleware
|
||||
*
|
||||
* Purpose: Verify JWT tokens and attach user info to requests
|
||||
* Applied to protected routes that require user authentication
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { appConfig } from '../config/app';
|
||||
import { AuthPayload } from '../@types';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
user?: AuthPayload;
|
||||
}
|
||||
|
||||
export function authenticate(req: AuthRequest, res: Response, next: NextFunction): void {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({ success: false, error: 'No token provided' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7); // Remove 'Bearer ' prefix
|
||||
|
||||
const decoded = jwt.verify(token, appConfig.jwtSecret) as AuthPayload;
|
||||
req.user = decoded;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ success: false, error: 'Invalid or expired token' });
|
||||
}
|
||||
}
|
||||
|
||||
export function isAdmin(req: AuthRequest, res: Response, next: NextFunction): void {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
res.status(403).json({ success: false, error: 'Admin access required' });
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
45
backend/src/middlewares/errorHandler.ts
Normal file
45
backend/src/middlewares/errorHandler.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Global Error Handler Middleware
|
||||
*
|
||||
* Purpose: Catch all errors, log them, and return consistent error responses
|
||||
* Applied as the last middleware in the Express chain
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { appConfig } from '../config/app';
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public isOperational = true
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export function errorHandler(
|
||||
err: Error | AppError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): void {
|
||||
console.error('Error:', err);
|
||||
|
||||
if (err instanceof AppError) {
|
||||
res.status(err.statusCode).json({
|
||||
success: false,
|
||||
error: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Unexpected errors
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: appConfig.env === 'production'
|
||||
? 'Internal server error'
|
||||
: err.message,
|
||||
});
|
||||
}
|
||||
22
backend/src/middlewares/requestLogger.ts
Normal file
22
backend/src/middlewares/requestLogger.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Request Logger Middleware
|
||||
*
|
||||
* Purpose: Log all incoming requests with method, path, IP, and response time
|
||||
* Useful for debugging and monitoring API usage
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export function requestLogger(req: Request, res: Response, next: NextFunction): void {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
const { method, originalUrl, ip } = req;
|
||||
const { statusCode } = res;
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${method} ${originalUrl} - ${statusCode} - ${duration}ms - ${ip}`);
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
3
backend/src/models/.gitkeep
Normal file
3
backend/src/models/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# Models/Repositories go here
|
||||
# Database access layer and query methods
|
||||
# Example: Product.ts, User.ts, Order.ts
|
||||
3
backend/src/routes/.gitkeep
Normal file
3
backend/src/routes/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# Route definitions go here
|
||||
# Maps URLs to controllers and applies middleware
|
||||
# Example: products.ts, auth.ts, users.ts
|
||||
62
backend/src/server.ts
Normal file
62
backend/src/server.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Backend Entry Point
|
||||
*
|
||||
* Purpose: Initialize Express app, apply middleware, mount routes, start server
|
||||
* This is where the entire backend application comes together
|
||||
*/
|
||||
|
||||
import express, { Application } from 'express';
|
||||
import cors from 'cors';
|
||||
import helmet from 'helmet';
|
||||
import compression from 'compression';
|
||||
import { appConfig } from './config/app';
|
||||
import { requestLogger } from './middlewares/requestLogger';
|
||||
import { errorHandler } from './middlewares/errorHandler';
|
||||
|
||||
// Initialize Express app
|
||||
const app: Application = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: appConfig.corsOrigin }));
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// Request logging
|
||||
app.use(requestLogger);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Server is running',
|
||||
environment: appConfig.env,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes
|
||||
// TODO: Import and mount your route files here
|
||||
// Example:
|
||||
// import authRoutes from './routes/auth';
|
||||
// import productRoutes from './routes/products';
|
||||
// app.use('/api/auth', authRoutes);
|
||||
// app.use('/api/products', productRoutes);
|
||||
|
||||
// Error handling (must be last)
|
||||
app.use(errorHandler);
|
||||
|
||||
// Start server
|
||||
const PORT = appConfig.port;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Backend server running on http://localhost:${PORT}`);
|
||||
console.log(`📝 Environment: ${appConfig.env}`);
|
||||
console.log(`🔗 CORS enabled for: ${appConfig.corsOrigin}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
3
backend/src/services/.gitkeep
Normal file
3
backend/src/services/.gitkeep
Normal file
@@ -0,0 +1,3 @@
|
||||
# Services go here
|
||||
# Contains business logic, data processing, and orchestration
|
||||
# Example: productService.ts, authService.ts, emailService.ts
|
||||
54
backend/src/validators/productValidator.ts
Normal file
54
backend/src/validators/productValidator.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Product Validation Schemas
|
||||
*
|
||||
* Purpose: Validate product-related request data before it reaches controllers
|
||||
* Prevents invalid data from entering the system
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export const createProductSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().min(10).max(5000),
|
||||
price: z.number().positive(),
|
||||
category: z.string().min(1),
|
||||
stock: z.number().int().nonnegative(),
|
||||
images: z.array(z.string().url()).optional(),
|
||||
});
|
||||
|
||||
export const updateProductSchema = createProductSchema.partial();
|
||||
|
||||
export function validateCreateProduct(req: Request, res: Response, next: NextFunction): void {
|
||||
try {
|
||||
createProductSchema.parse(req.body);
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateUpdateProduct(req: Request, res: Response, next: NextFunction): void {
|
||||
try {
|
||||
updateProductSchema.parse(req.body);
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: error.errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Media Library Database Test Script
|
||||
* Tests all database operations for uploads and folders
|
||||
*/
|
||||
|
||||
const { pool } = require("./config/database");
|
||||
const logger = require("./config/logger");
|
||||
|
||||
async function testDatabaseOperations() {
|
||||
console.log("\n🧪 Testing Media Library Database Operations\n");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
try {
|
||||
// Test 1: Check database connection
|
||||
console.log("\n1️⃣ Testing Database Connection...");
|
||||
const connectionTest = await pool.query("SELECT NOW()");
|
||||
console.log(" ✅ Database connected:", connectionTest.rows[0].now);
|
||||
|
||||
// Test 2: Check uploads table structure
|
||||
console.log("\n2️⃣ Checking uploads table structure...");
|
||||
const uploadsSchema = await pool.query(`
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'uploads'
|
||||
ORDER BY ordinal_position
|
||||
`);
|
||||
console.log(" ✅ Uploads table columns:");
|
||||
uploadsSchema.rows.forEach((col) => {
|
||||
console.log(
|
||||
` - ${col.column_name} (${col.data_type}, nullable: ${col.is_nullable})`
|
||||
);
|
||||
});
|
||||
|
||||
// Test 3: Check media_folders table structure
|
||||
console.log("\n3️⃣ Checking media_folders table structure...");
|
||||
const foldersSchema = await pool.query(`
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'media_folders'
|
||||
ORDER BY ordinal_position
|
||||
`);
|
||||
console.log(" ✅ Media folders table columns:");
|
||||
foldersSchema.rows.forEach((col) => {
|
||||
console.log(
|
||||
` - ${col.column_name} (${col.data_type}, nullable: ${col.is_nullable})`
|
||||
);
|
||||
});
|
||||
|
||||
// Test 4: Check foreign key constraints
|
||||
console.log("\n4️⃣ Checking foreign key constraints...");
|
||||
const constraints = await pool.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name IN ('uploads', 'media_folders')
|
||||
`);
|
||||
console.log(" ✅ Foreign key constraints:");
|
||||
constraints.rows.forEach((fk) => {
|
||||
console.log(
|
||||
` - ${fk.table_name}.${fk.column_name} → ${fk.foreign_table_name}.${fk.foreign_column_name}`
|
||||
);
|
||||
});
|
||||
|
||||
// Test 5: Count existing data
|
||||
console.log("\n5️⃣ Counting existing data...");
|
||||
const fileCount = await pool.query("SELECT COUNT(*) as count FROM uploads");
|
||||
const folderCount = await pool.query(
|
||||
"SELECT COUNT(*) as count FROM media_folders"
|
||||
);
|
||||
console.log(` ✅ Total files: ${fileCount.rows[0].count}`);
|
||||
console.log(` ✅ Total folders: ${folderCount.rows[0].count}`);
|
||||
|
||||
// Test 6: List all files
|
||||
if (parseInt(fileCount.rows[0].count) > 0) {
|
||||
console.log("\n6️⃣ Listing all files in database...");
|
||||
const files = await pool.query(`
|
||||
SELECT id, original_name, file_size, folder_id, uploaded_by, created_at
|
||||
FROM uploads
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
`);
|
||||
console.log(" ✅ Recent files:");
|
||||
files.rows.forEach((file) => {
|
||||
const size = (file.file_size / 1024).toFixed(2) + " KB";
|
||||
const folder = file.folder_id ? `Folder #${file.folder_id}` : "Root";
|
||||
console.log(
|
||||
` - [ID: ${file.id}] ${file.original_name} (${size}) - ${folder}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 7: List all folders
|
||||
if (parseInt(folderCount.rows[0].count) > 0) {
|
||||
console.log("\n7️⃣ Listing all folders in database...");
|
||||
const folders = await pool.query(`
|
||||
SELECT id, name, parent_id, created_by, created_at,
|
||||
(SELECT COUNT(*) FROM uploads WHERE folder_id = media_folders.id) as file_count
|
||||
FROM media_folders
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
console.log(" ✅ Folders:");
|
||||
folders.rows.forEach((folder) => {
|
||||
const parent = folder.parent_id
|
||||
? `Parent #${folder.parent_id}`
|
||||
: "Root";
|
||||
console.log(
|
||||
` - [ID: ${folder.id}] ${folder.name} (${folder.file_count} files) - ${parent}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Test 8: Test folder query with file counts
|
||||
console.log("\n8️⃣ Testing folder query with file counts...");
|
||||
const foldersWithCounts = await pool.query(`
|
||||
SELECT
|
||||
mf.*,
|
||||
COUNT(u.id) as file_count
|
||||
FROM media_folders mf
|
||||
LEFT JOIN uploads u ON u.folder_id = mf.id
|
||||
GROUP BY mf.id
|
||||
ORDER BY mf.created_at DESC
|
||||
`);
|
||||
console.log(
|
||||
` ✅ Query returned ${foldersWithCounts.rows.length} folders with accurate file counts`
|
||||
);
|
||||
|
||||
// Test 9: Test cascade delete (dry run)
|
||||
console.log("\n9️⃣ Testing cascade delete rules...");
|
||||
const cascadeRules = await pool.query(`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
rc.delete_rule
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
WHERE tc.table_name IN ('uploads', 'media_folders')
|
||||
AND tc.constraint_type = 'FOREIGN KEY'
|
||||
`);
|
||||
console.log(" ✅ Delete rules:");
|
||||
cascadeRules.rows.forEach((rule) => {
|
||||
console.log(` - ${rule.constraint_name}: ${rule.delete_rule}`);
|
||||
});
|
||||
|
||||
// Test 10: Verify indexes
|
||||
console.log("\n🔟 Checking database indexes...");
|
||||
const indexes = await pool.query(`
|
||||
SELECT
|
||||
tablename,
|
||||
indexname,
|
||||
indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename IN ('uploads', 'media_folders')
|
||||
AND schemaname = 'public'
|
||||
ORDER BY tablename, indexname
|
||||
`);
|
||||
console.log(" ✅ Indexes:");
|
||||
indexes.rows.forEach((idx) => {
|
||||
console.log(` - ${idx.tablename}.${idx.indexname}`);
|
||||
});
|
||||
|
||||
console.log("\n" + "=".repeat(60));
|
||||
console.log("✅ All database tests passed successfully!\n");
|
||||
|
||||
console.log("📊 Summary:");
|
||||
console.log(` - Database: Connected and operational`);
|
||||
console.log(
|
||||
` - Tables: uploads (${uploadsSchema.rows.length} columns), media_folders (${foldersSchema.rows.length} columns)`
|
||||
);
|
||||
console.log(
|
||||
` - Data: ${fileCount.rows[0].count} files, ${folderCount.rows[0].count} folders`
|
||||
);
|
||||
console.log(
|
||||
` - Constraints: ${constraints.rows.length} foreign keys configured`
|
||||
);
|
||||
console.log(` - Indexes: ${indexes.rows.length} indexes for performance`);
|
||||
console.log(
|
||||
"\n✅ Media library database is properly configured and operational!\n"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("\n❌ Database test failed:", error.message);
|
||||
console.error("Stack trace:", error.stack);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests
|
||||
testDatabaseOperations();
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Backend Navigation Test Script
|
||||
|
||||
echo "=========================================="
|
||||
echo " Testing Backend Admin Panel Navigation"
|
||||
echo "=========================================="
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test if backend is running
|
||||
echo -e "\n1. Checking if backend server is running..."
|
||||
if curl -s http://localhost:5000/health > /dev/null; then
|
||||
echo -e "${GREEN}✓ Backend server is running${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ Backend server is not responding${NC}"
|
||||
echo "Please start the backend server first:"
|
||||
echo " cd /media/pts/Website/SkyArtShop/backend && npm start"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if admin files are accessible
|
||||
echo -e "\n2. Checking admin panel files..."
|
||||
pages=("dashboard.html" "products.html" "portfolio.html" "blog.html" "pages.html" "menu.html" "settings.html" "users.html" "homepage.html")
|
||||
|
||||
for page in "${pages[@]}"; do
|
||||
if curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/admin/$page | grep -q "200\|304"; then
|
||||
echo -e "${GREEN}✓ /admin/$page accessible${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ /admin/$page not found${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check API endpoints
|
||||
echo -e "\n3. Checking API endpoints..."
|
||||
endpoints=(
|
||||
"/api/admin/session"
|
||||
"/api/products"
|
||||
"/api/portfolio/projects"
|
||||
"/api/blog/posts"
|
||||
"/api/pages"
|
||||
"/api/menu"
|
||||
"/api/homepage/settings"
|
||||
)
|
||||
|
||||
for endpoint in "${endpoints[@]}"; do
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000$endpoint)
|
||||
if [ "$status" == "200" ] || [ "$status" == "401" ]; then
|
||||
echo -e "${GREEN}✓ $endpoint responding (HTTP $status)${NC}"
|
||||
else
|
||||
echo -e "${RED}✗ $endpoint not responding properly (HTTP $status)${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "\n=========================================="
|
||||
echo " Test Complete!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Next Steps:"
|
||||
echo "1. Login to the admin panel at http://localhost:5000/admin/login.html"
|
||||
echo "2. After login, navigate through different sections"
|
||||
echo "3. Verify you stay logged in when clicking navigation links"
|
||||
echo "4. Create/Edit content in each section"
|
||||
echo "5. Verify changes appear on the frontend"
|
||||
echo ""
|
||||
@@ -1,219 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Test Editor Resize</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.editor-resizable {
|
||||
position: relative;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: visible;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.editor-resize-handle {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: nwse-resize;
|
||||
background: linear-gradient(135deg, transparent 50%, #667eea 50%);
|
||||
z-index: 1000;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.editor-resize-handle:hover {
|
||||
background: linear-gradient(135deg, transparent 50%, #5568d3 50%);
|
||||
}
|
||||
.editor-resize-handle:active {
|
||||
background: linear-gradient(135deg, transparent 50%, #4451b8 50%);
|
||||
}
|
||||
.editor-content {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
}
|
||||
.editable-content {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background: white;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
resize: none;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #2196f3;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-box {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h2>🧪 Editor Resize Test - Full Functionality</h2>
|
||||
|
||||
<div class="status">
|
||||
<strong>✅ Test Instructions:</strong> Drag the small blue triangle in
|
||||
the bottom-right corner to resize. You should be able to:
|
||||
<ul style="margin: 10px 0 0 20px; padding: 0">
|
||||
<li>Resize multiple times (up and down)</li>
|
||||
<li>Edit/type in the expanded area</li>
|
||||
<li>See smooth resizing with no jumps</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>📝 Try This:</strong> Drag editor bigger → Type text in new
|
||||
space → Drag smaller → Drag bigger again
|
||||
</div>
|
||||
|
||||
<h3>Test 1: Editable Text Area</h3>
|
||||
<div class="editor-resizable">
|
||||
<textarea
|
||||
id="editor1"
|
||||
class="editable-content"
|
||||
placeholder="Type here to test editing in expanded area..."
|
||||
>
|
||||
This is an EDITABLE text area.
|
||||
|
||||
Try this:
|
||||
1. Drag the corner to make it BIGGER
|
||||
2. Click here and TYPE NEW TEXT in the expanded area
|
||||
3. Drag to make it SMALLER
|
||||
4. Drag to make it BIGGER again
|
||||
|
||||
You should be able to resize multiple times and edit anywhere in the expanded space!</textarea
|
||||
>
|
||||
<div class="editor-resize-handle" data-target="editor1"></div>
|
||||
</div>
|
||||
|
||||
<h3>Test 2: Contact Fields Scrollable</h3>
|
||||
<div class="editor-resizable">
|
||||
<div id="editor2" class="editor-content">
|
||||
<p><strong>📞 Contact Information</strong></p>
|
||||
<p>Phone: (555) 123-4567</p>
|
||||
<p>Email: contact@example.com</p>
|
||||
<p>Address: 123 Main St, City, State 12345</p>
|
||||
<br />
|
||||
<p><strong>🕐 Business Hours</strong></p>
|
||||
<p>Monday - Friday: 9:00 AM - 6:00 PM</p>
|
||||
<p>Saturday: 10:00 AM - 4:00 PM</p>
|
||||
<p>Sunday: Closed</p>
|
||||
<br />
|
||||
<p><strong>Extra Content for Testing</strong></p>
|
||||
<p>This content should remain scrollable.</p>
|
||||
<p>Resize the box to see more or less content.</p>
|
||||
<p>The scrollbar should work properly.</p>
|
||||
</div>
|
||||
<div class="editor-resize-handle" data-target="editor2"></div>
|
||||
</div>
|
||||
|
||||
<div class="status" id="resizeStatus">
|
||||
<strong>Status:</strong> Ready to test - Drag the corner handles!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple Drag-to-Resize for Editor Content - EXACT SAME CODE AS ADMIN
|
||||
(function () {
|
||||
let resizeState = null;
|
||||
const statusEl = document.getElementById("resizeStatus");
|
||||
|
||||
document.addEventListener("mousedown", function (e) {
|
||||
if (e.target.classList.contains("editor-resize-handle")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const targetId = e.target.getAttribute("data-target");
|
||||
const targetElement = document.getElementById(targetId);
|
||||
|
||||
if (!targetElement) return;
|
||||
|
||||
resizeState = {
|
||||
target: targetElement,
|
||||
handle: e.target,
|
||||
startY: e.clientY,
|
||||
startHeight: targetElement.offsetHeight,
|
||||
};
|
||||
|
||||
document.body.style.cursor = "nwse-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
e.target.style.pointerEvents = "none";
|
||||
statusEl.innerHTML =
|
||||
"<strong>Status:</strong> 🔄 Resizing " +
|
||||
targetId +
|
||||
"... (Height: " +
|
||||
Math.round(resizeState.startHeight) +
|
||||
"px)";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mousemove", function (e) {
|
||||
if (resizeState) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const deltaY = e.clientY - resizeState.startY;
|
||||
const newHeight = Math.max(
|
||||
200,
|
||||
Math.min(1200, resizeState.startHeight + deltaY)
|
||||
);
|
||||
|
||||
resizeState.target.style.height = newHeight + "px";
|
||||
statusEl.innerHTML =
|
||||
"<strong>Status:</strong> 📏 Resizing to " +
|
||||
Math.round(newHeight) +
|
||||
"px (Delta: " +
|
||||
Math.round(deltaY) +
|
||||
"px)";
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", function () {
|
||||
if (resizeState) {
|
||||
const finalHeight = resizeState.target.offsetHeight;
|
||||
statusEl.innerHTML =
|
||||
"<strong>Status:</strong> ✅ Resize complete! Final height: " +
|
||||
finalHeight +
|
||||
"px. Try typing in the editor or resizing again!";
|
||||
|
||||
resizeState.handle.style.pointerEvents = "";
|
||||
resizeState = null;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Portfolio API Response
|
||||
*/
|
||||
|
||||
const { query } = require("./config/database");
|
||||
|
||||
async function testPortfolioAPI() {
|
||||
console.log("🧪 Testing Portfolio API Data...\n");
|
||||
|
||||
try {
|
||||
// Simulate what the API endpoint does
|
||||
const result = await query(
|
||||
"SELECT id, title, description, imageurl, category, isactive, createdat FROM portfolioprojects ORDER BY createdat DESC"
|
||||
);
|
||||
|
||||
console.log("📊 API Response Data:\n");
|
||||
result.rows.forEach((p, index) => {
|
||||
const status = p.isactive ? "✓ Active" : "✗ Inactive";
|
||||
const statusColor = p.isactive ? "\x1b[32m" : "\x1b[31m";
|
||||
const reset = "\x1b[0m";
|
||||
|
||||
console.log(
|
||||
`${index + 1}. ${statusColor}${status}${reset} | ID: ${p.id} | ${
|
||||
p.title
|
||||
}`
|
||||
);
|
||||
console.log(
|
||||
` isactive value: ${p.isactive} (type: ${typeof p.isactive})`
|
||||
);
|
||||
console.log(` category: ${p.category || "N/A"}`);
|
||||
console.log("");
|
||||
});
|
||||
|
||||
const activeCount = result.rows.filter((p) => p.isactive).length;
|
||||
const inactiveCount = result.rows.length - activeCount;
|
||||
|
||||
console.log(`\n📈 Summary:`);
|
||||
console.log(` Total: ${result.rows.length} projects`);
|
||||
console.log(` ✓ Active: ${activeCount}`);
|
||||
console.log(` ✗ Inactive: ${inactiveCount}\n`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("❌ Error:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
testPortfolioAPI();
|
||||
@@ -1,337 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test Products API with Color Variants
|
||||
* Tests the new product creation and management features
|
||||
*/
|
||||
|
||||
const axios = require("axios");
|
||||
|
||||
const API_URL = process.env.API_URL || "http://localhost:5000/api";
|
||||
|
||||
// Test data
|
||||
const testProduct = {
|
||||
name: "Vibrant Sunset Canvas Art",
|
||||
shortdescription: "Beautiful hand-painted sunset artwork on premium canvas",
|
||||
description:
|
||||
"<p>This stunning piece captures the beauty of a <strong>vibrant sunset</strong> over the ocean. Hand-painted with premium acrylics on gallery-wrapped canvas.</p><p><strong>Features:</strong></p><ul><li>Gallery-wrapped canvas</li><li>Ready to hang</li><li>Signed by artist</li></ul>",
|
||||
price: 249.99,
|
||||
stockquantity: 10,
|
||||
category: "Canvas Art",
|
||||
sku: "ART-SUNSET-001",
|
||||
weight: 2.5,
|
||||
dimensions: "24x36 inches",
|
||||
material: "Acrylic on Canvas",
|
||||
isactive: true,
|
||||
isfeatured: true,
|
||||
isbestseller: false,
|
||||
images: [
|
||||
{
|
||||
image_url: "/uploads/products/sunset-main.jpg",
|
||||
color_variant: "Original",
|
||||
alt_text: "Vibrant Sunset Canvas - Main View",
|
||||
display_order: 0,
|
||||
is_primary: true,
|
||||
},
|
||||
{
|
||||
image_url: "/uploads/products/sunset-blue.jpg",
|
||||
color_variant: "Ocean Blue",
|
||||
alt_text: "Vibrant Sunset Canvas - Blue Variant",
|
||||
display_order: 1,
|
||||
is_primary: false,
|
||||
},
|
||||
{
|
||||
image_url: "/uploads/products/sunset-warm.jpg",
|
||||
color_variant: "Warm Tones",
|
||||
alt_text: "Vibrant Sunset Canvas - Warm Variant",
|
||||
display_order: 2,
|
||||
is_primary: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let createdProductId = null;
|
||||
let sessionCookie = null;
|
||||
|
||||
async function login() {
|
||||
try {
|
||||
console.log("🔐 Logging in...");
|
||||
const response = await axios.post(
|
||||
`${API_URL}/auth/login`,
|
||||
{
|
||||
email: process.env.ADMIN_EMAIL || "admin@skyartshop.com",
|
||||
password: process.env.ADMIN_PASSWORD || "admin123",
|
||||
},
|
||||
{
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}
|
||||
);
|
||||
|
||||
sessionCookie = response.headers["set-cookie"];
|
||||
console.log("✅ Login successful\n");
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("❌ Login failed:", error.response?.data || error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProduct() {
|
||||
try {
|
||||
console.log("📦 Creating new product...");
|
||||
console.log("Product name:", testProduct.name);
|
||||
console.log(
|
||||
"Color variants:",
|
||||
testProduct.images.map((img) => img.color_variant).join(", ")
|
||||
);
|
||||
|
||||
const response = await axios.post(
|
||||
`${API_URL}/admin/products`,
|
||||
testProduct,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
createdProductId = response.data.product.id;
|
||||
console.log("✅ Product created successfully!");
|
||||
console.log("Product ID:", createdProductId);
|
||||
console.log("Images count:", response.data.product.images?.length || 0);
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Product creation failed:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getProduct() {
|
||||
try {
|
||||
console.log("📖 Fetching product details...");
|
||||
|
||||
const response = await axios.get(
|
||||
`${API_URL}/admin/products/${createdProductId}`,
|
||||
{
|
||||
headers: { Cookie: sessionCookie },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const product = response.data.product;
|
||||
console.log("✅ Product retrieved successfully!");
|
||||
console.log("Name:", product.name);
|
||||
console.log("Price:", product.price);
|
||||
console.log("SKU:", product.sku);
|
||||
console.log("Stock:", product.stockquantity);
|
||||
console.log("Active:", product.isactive);
|
||||
console.log("Featured:", product.isfeatured);
|
||||
console.log("Images:");
|
||||
|
||||
if (product.images && product.images.length > 0) {
|
||||
product.images.forEach((img, idx) => {
|
||||
console.log(
|
||||
` ${idx + 1}. ${img.color_variant || "Default"} - ${
|
||||
img.image_url
|
||||
} ${img.is_primary ? "(Primary)" : ""}`
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Failed to fetch product:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProduct() {
|
||||
try {
|
||||
console.log("✏️ Updating product...");
|
||||
|
||||
const updateData = {
|
||||
price: 199.99,
|
||||
stockquantity: 15,
|
||||
isbestseller: true,
|
||||
images: [
|
||||
...testProduct.images,
|
||||
{
|
||||
image_url: "/uploads/products/sunset-purple.jpg",
|
||||
color_variant: "Purple Haze",
|
||||
alt_text: "Vibrant Sunset Canvas - Purple Variant",
|
||||
display_order: 3,
|
||||
is_primary: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await axios.put(
|
||||
`${API_URL}/admin/products/${createdProductId}`,
|
||||
updateData,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Cookie: sessionCookie,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
console.log("✅ Product updated successfully!");
|
||||
console.log("New price:", response.data.product.price);
|
||||
console.log("New stock:", response.data.product.stockquantity);
|
||||
console.log("Bestseller:", response.data.product.isbestseller);
|
||||
console.log("Total images:", response.data.product.images?.length || 0);
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Product update failed:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listProducts() {
|
||||
try {
|
||||
console.log("📋 Listing all products...");
|
||||
|
||||
const response = await axios.get(`${API_URL}/admin/products`, {
|
||||
headers: { Cookie: sessionCookie },
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
console.log(`✅ Found ${response.data.products.length} products`);
|
||||
response.data.products.forEach((p, idx) => {
|
||||
console.log(
|
||||
`${idx + 1}. ${p.name} - $${p.price} (${p.image_count || 0} images)`
|
||||
);
|
||||
});
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Failed to list products:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getPublicProduct() {
|
||||
try {
|
||||
console.log("🌐 Fetching product from public API...");
|
||||
|
||||
const response = await axios.get(
|
||||
`${API_URL}/public/products/${createdProductId}`
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const product = response.data.product;
|
||||
console.log("✅ Public product retrieved!");
|
||||
console.log("Name:", product.name);
|
||||
console.log(
|
||||
"Short description:",
|
||||
product.shortdescription?.substring(0, 50) + "..."
|
||||
);
|
||||
console.log("Color variants available:");
|
||||
|
||||
const variants = [
|
||||
...new Set(
|
||||
product.images?.map((img) => img.color_variant).filter(Boolean)
|
||||
),
|
||||
];
|
||||
variants.forEach((variant) => {
|
||||
console.log(` - ${variant}`);
|
||||
});
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Failed to fetch public product:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProduct() {
|
||||
try {
|
||||
console.log("🗑️ Deleting test product...");
|
||||
|
||||
const response = await axios.delete(
|
||||
`${API_URL}/admin/products/${createdProductId}`,
|
||||
{
|
||||
headers: { Cookie: sessionCookie },
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
console.log("✅ Product deleted successfully!");
|
||||
console.log("");
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"❌ Product deletion failed:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function runTests() {
|
||||
console.log("=".repeat(60));
|
||||
console.log(" PRODUCTS API TEST - Color Variants & Rich Text");
|
||||
console.log("=".repeat(60));
|
||||
console.log("");
|
||||
|
||||
const steps = [
|
||||
{ name: "Login", fn: login },
|
||||
{ name: "Create Product", fn: createProduct },
|
||||
{ name: "Get Product", fn: getProduct },
|
||||
{ name: "Update Product", fn: updateProduct },
|
||||
{ name: "List Products", fn: listProducts },
|
||||
{ name: "Get Public Product", fn: getPublicProduct },
|
||||
{ name: "Delete Product", fn: deleteProduct },
|
||||
];
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const step of steps) {
|
||||
const success = await step.fn();
|
||||
if (success) {
|
||||
passed++;
|
||||
} else {
|
||||
failed++;
|
||||
console.log(`⚠️ Stopping tests due to failure in: ${step.name}\n`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("=".repeat(60));
|
||||
console.log(`TEST RESULTS: ${passed} passed, ${failed} failed`);
|
||||
console.log("=".repeat(60));
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error("Fatal error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,168 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Test Products API with curl
|
||||
|
||||
API_URL="http://localhost:5000/api"
|
||||
SESSION_FILE="/tmp/skyart_session.txt"
|
||||
|
||||
echo "============================================================"
|
||||
echo " PRODUCTS API TEST - Color Variants & Rich Text"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
|
||||
# Test 1: Login
|
||||
echo "🔐 Test 1: Login..."
|
||||
LOGIN_RESPONSE=$(curl -s -c "$SESSION_FILE" -X POST "$API_URL/admin/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@example.com","password":"admin123"}')
|
||||
|
||||
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
|
||||
echo "✅ Login successful"
|
||||
else
|
||||
echo "❌ Login failed"
|
||||
echo "$LOGIN_RESPONSE"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 2: Create Product
|
||||
echo "📦 Test 2: Creating product with color variants..."
|
||||
CREATE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X POST "$API_URL/admin/products" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Sunset Canvas Art",
|
||||
"shortdescription": "Beautiful hand-painted sunset artwork",
|
||||
"description": "<p>This stunning piece captures the beauty of a <strong>vibrant sunset</strong>.</p><ul><li>Gallery-wrapped canvas</li><li>Ready to hang</li></ul>",
|
||||
"price": 249.99,
|
||||
"stockquantity": 10,
|
||||
"category": "Canvas Art",
|
||||
"sku": "ART-TEST-001",
|
||||
"weight": 2.5,
|
||||
"dimensions": "24x36 inches",
|
||||
"material": "Acrylic on Canvas",
|
||||
"isactive": true,
|
||||
"isfeatured": true,
|
||||
"isbestseller": false,
|
||||
"images": [
|
||||
{
|
||||
"image_url": "/uploads/test-sunset-main.jpg",
|
||||
"color_variant": "Original",
|
||||
"alt_text": "Sunset Canvas - Main",
|
||||
"display_order": 0,
|
||||
"is_primary": true
|
||||
},
|
||||
{
|
||||
"image_url": "/uploads/test-sunset-blue.jpg",
|
||||
"color_variant": "Ocean Blue",
|
||||
"alt_text": "Sunset Canvas - Blue",
|
||||
"display_order": 1
|
||||
},
|
||||
{
|
||||
"image_url": "/uploads/test-sunset-warm.jpg",
|
||||
"color_variant": "Warm Tones",
|
||||
"alt_text": "Sunset Canvas - Warm",
|
||||
"display_order": 2
|
||||
}
|
||||
]
|
||||
}')
|
||||
|
||||
PRODUCT_ID=$(echo "$CREATE_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$PRODUCT_ID" ]; then
|
||||
echo "✅ Product created successfully"
|
||||
echo " Product ID: $PRODUCT_ID"
|
||||
IMAGE_COUNT=$(echo "$CREATE_RESPONSE" | grep -o '"image_url"' | wc -l)
|
||||
echo " Images: $IMAGE_COUNT"
|
||||
else
|
||||
echo "❌ Product creation failed"
|
||||
echo "$CREATE_RESPONSE" | head -50
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: Get Product
|
||||
echo "📖 Test 3: Fetching product details..."
|
||||
GET_RESPONSE=$(curl -s -b "$SESSION_FILE" "$API_URL/admin/products/$PRODUCT_ID")
|
||||
|
||||
if echo "$GET_RESPONSE" | grep -q '"success":true'; then
|
||||
echo "✅ Product retrieved successfully"
|
||||
echo " Name: $(echo "$GET_RESPONSE" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)"
|
||||
echo " Price: $(echo "$GET_RESPONSE" | grep -o '"price":"[^"]*"' | head -1 | cut -d'"' -f4)"
|
||||
echo " Color variants:"
|
||||
echo "$GET_RESPONSE" | grep -o '"color_variant":"[^"]*"' | cut -d'"' -f4 | while read variant; do
|
||||
echo " - $variant"
|
||||
done
|
||||
else
|
||||
echo "❌ Failed to retrieve product"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: Update Product
|
||||
echo "✏️ Test 4: Updating product..."
|
||||
UPDATE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X PUT "$API_URL/admin/products/$PRODUCT_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"price": 199.99,
|
||||
"stockquantity": 15,
|
||||
"isbestseller": true
|
||||
}')
|
||||
|
||||
if echo "$UPDATE_RESPONSE" | grep -q '"success":true'; then
|
||||
echo "✅ Product updated successfully"
|
||||
echo " New price: $(echo "$UPDATE_RESPONSE" | grep -o '"price":"[^"]*"' | head -1 | cut -d'"' -f4)"
|
||||
else
|
||||
echo "❌ Product update failed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: List Products
|
||||
echo "📋 Test 5: Listing all products..."
|
||||
LIST_RESPONSE=$(curl -s -b "$SESSION_FILE" "$API_URL/admin/products")
|
||||
|
||||
PRODUCT_COUNT=$(echo "$LIST_RESPONSE" | grep -o '"id"' | wc -l)
|
||||
echo "✅ Found $PRODUCT_COUNT products"
|
||||
echo ""
|
||||
|
||||
# Test 6: Public API
|
||||
echo "🌐 Test 6: Testing public API..."
|
||||
PUBLIC_RESPONSE=$(curl -s "$API_URL/products/$PRODUCT_ID")
|
||||
|
||||
if echo "$PUBLIC_RESPONSE" | grep -q '"success":true'; then
|
||||
echo "✅ Public product retrieved"
|
||||
echo " Available color variants:"
|
||||
echo "$PUBLIC_RESPONSE" | grep -o '"color_variant":"[^"]*"' | cut -d'"' -f4 | sort -u | while read variant; do
|
||||
echo " - $variant"
|
||||
done
|
||||
else
|
||||
echo "❌ Failed to retrieve public product"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 7: Delete Product
|
||||
echo "🗑️ Test 7: Cleaning up test product..."
|
||||
DELETE_RESPONSE=$(curl -s -b "$SESSION_FILE" -X DELETE "$API_URL/admin/products/$PRODUCT_ID")
|
||||
|
||||
if echo "$DELETE_RESPONSE" | grep -q '"success":true'; then
|
||||
echo "✅ Test product deleted"
|
||||
else
|
||||
echo "❌ Product deletion failed"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Cleanup
|
||||
rm -f "$SESSION_FILE"
|
||||
|
||||
echo "============================================================"
|
||||
echo " ALL TESTS COMPLETED SUCCESSFULLY! ✅"
|
||||
echo "============================================================"
|
||||
echo ""
|
||||
echo "Features Verified:"
|
||||
echo " ✅ Product creation with color variants"
|
||||
echo " ✅ Rich text HTML description"
|
||||
echo " ✅ Multiple images per product"
|
||||
echo " ✅ Color variant assignments"
|
||||
echo " ✅ Active/Featured/Bestseller flags"
|
||||
echo " ✅ Product metadata (SKU, weight, dimensions, material)"
|
||||
echo " ✅ Product updates"
|
||||
echo " ✅ Public API access"
|
||||
echo " ✅ Product deletion with cascade"
|
||||
echo ""
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Script: Upload Database Integration
|
||||
*
|
||||
* This script tests that file uploads are properly recorded in PostgreSQL
|
||||
*/
|
||||
|
||||
const { pool } = require("./config/database");
|
||||
|
||||
async function testUploadDatabase() {
|
||||
console.log("🔍 Testing Upload Database Integration...\n");
|
||||
|
||||
try {
|
||||
// Test 1: Check if uploads table exists
|
||||
console.log("1️⃣ Checking uploads table...");
|
||||
const tableCheck = await pool.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'uploads'
|
||||
);
|
||||
`);
|
||||
|
||||
if (tableCheck.rows[0].exists) {
|
||||
console.log(" ✅ uploads table exists\n");
|
||||
} else {
|
||||
console.log(" ❌ uploads table not found\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Test 2: Check table structure
|
||||
console.log("2️⃣ Checking table structure...");
|
||||
const columns = await pool.query(`
|
||||
SELECT column_name, data_type, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'uploads'
|
||||
ORDER BY ordinal_position;
|
||||
`);
|
||||
|
||||
console.log(" Columns:");
|
||||
columns.rows.forEach((col) => {
|
||||
console.log(
|
||||
` - ${col.column_name} (${col.data_type}) ${
|
||||
col.is_nullable === "YES" ? "NULL" : "NOT NULL"
|
||||
}`
|
||||
);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// Test 3: Check indexes
|
||||
console.log("3️⃣ Checking indexes...");
|
||||
const indexes = await pool.query(`
|
||||
SELECT indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE tablename = 'uploads';
|
||||
`);
|
||||
|
||||
console.log(` Found ${indexes.rows.length} index(es):`);
|
||||
indexes.rows.forEach((idx) => {
|
||||
console.log(` - ${idx.indexname}`);
|
||||
});
|
||||
console.log();
|
||||
|
||||
// Test 4: Query existing uploads
|
||||
console.log("4️⃣ Querying existing uploads...");
|
||||
const uploads = await pool.query(`
|
||||
SELECT id, filename, original_name, file_size, mime_type, created_at
|
||||
FROM uploads
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10;
|
||||
`);
|
||||
|
||||
console.log(` Found ${uploads.rows.length} upload(s) in database:`);
|
||||
if (uploads.rows.length > 0) {
|
||||
uploads.rows.forEach((upload) => {
|
||||
console.log(
|
||||
` - [${upload.id}] ${upload.original_name} (${upload.filename})`
|
||||
);
|
||||
console.log(
|
||||
` Size: ${(upload.file_size / 1024).toFixed(2)}KB | Type: ${
|
||||
upload.mime_type
|
||||
}`
|
||||
);
|
||||
console.log(` Uploaded: ${upload.created_at}`);
|
||||
});
|
||||
} else {
|
||||
console.log(" No uploads found yet. Upload a file to test!");
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Test 5: Check foreign key constraint
|
||||
console.log("5️⃣ Checking foreign key constraints...");
|
||||
const fkeys = await pool.query(`
|
||||
SELECT conname, conrelid::regclass, confrelid::regclass
|
||||
FROM pg_constraint
|
||||
WHERE contype = 'f' AND conrelid = 'uploads'::regclass;
|
||||
`);
|
||||
|
||||
if (fkeys.rows.length > 0) {
|
||||
console.log(` Found ${fkeys.rows.length} foreign key(s):`);
|
||||
fkeys.rows.forEach((fk) => {
|
||||
console.log(` - ${fk.conname}: ${fk.conrelid} -> ${fk.confrelid}`);
|
||||
});
|
||||
} else {
|
||||
console.log(" No foreign keys found");
|
||||
}
|
||||
console.log();
|
||||
|
||||
console.log("✅ Database integration test complete!\n");
|
||||
console.log("📋 Summary:");
|
||||
console.log(" - Database: skyartshop");
|
||||
console.log(" - Table: uploads");
|
||||
console.log(" - Records: " + uploads.rows.length);
|
||||
console.log(" - Status: Ready for production ✨\n");
|
||||
} catch (error) {
|
||||
console.error("❌ Test failed:", error.message);
|
||||
console.error(error);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Run test
|
||||
testUploadDatabase().catch(console.error);
|
||||
56
backend/tsconfig.json
Normal file
56
backend/tsconfig.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
],
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
/* Type Checking */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
/* Output */
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
/* Path Aliases */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
],
|
||||
"@config/*": [
|
||||
"src/config/*"
|
||||
],
|
||||
"@controllers/*": [
|
||||
"src/controllers/*"
|
||||
],
|
||||
"@services/*": [
|
||||
"src/services/*"
|
||||
],
|
||||
"@models/*": [
|
||||
"src/models/*"
|
||||
],
|
||||
"@middlewares/*": [
|
||||
"src/middlewares/*"
|
||||
],
|
||||
"@helpers/*": [
|
||||
"src/helpers/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
43
backend/update-page-ids.js
Normal file
43
backend/update-page-ids.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const db = require("./config/database");
|
||||
|
||||
async function updatePageIdsToReadable() {
|
||||
try {
|
||||
console.log("Updating page IDs to readable format...\n");
|
||||
|
||||
// Get all pages
|
||||
const pages = await db.query("SELECT id, slug FROM pages");
|
||||
|
||||
for (const page of pages.rows) {
|
||||
const newId = `page-${page.slug}`;
|
||||
|
||||
if (page.id !== newId) {
|
||||
console.log(`Updating: ${page.slug}`);
|
||||
console.log(` Old ID: ${page.id}`);
|
||||
console.log(` New ID: ${newId}`);
|
||||
|
||||
await db.query("UPDATE pages SET id = $1 WHERE slug = $2", [
|
||||
newId,
|
||||
page.slug,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n✅ All page IDs updated to readable format!");
|
||||
console.log("\nVerifying updates...");
|
||||
|
||||
const updated = await db.query(
|
||||
"SELECT id, slug, title FROM pages ORDER BY slug"
|
||||
);
|
||||
console.log("\nCurrent page IDs:");
|
||||
updated.rows.forEach((p) => {
|
||||
console.log(` ${p.id.padEnd(25)} → ${p.title}`);
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error updating page IDs:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updatePageIdsToReadable();
|
||||
55
backend/utils/cacheInvalidation.js
Normal file
55
backend/utils/cacheInvalidation.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Cache Invalidation Helper
|
||||
* Add to admin routes to clear cache when data changes
|
||||
*/
|
||||
const { cache } = require("../middleware/cache");
|
||||
const logger = require("../config/logger");
|
||||
|
||||
/**
|
||||
* Invalidate product-related cache
|
||||
*/
|
||||
const invalidateProductCache = () => {
|
||||
cache.deletePattern("products");
|
||||
cache.deletePattern("featured");
|
||||
logger.debug("Product cache invalidated");
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate blog-related cache
|
||||
*/
|
||||
const invalidateBlogCache = () => {
|
||||
cache.deletePattern("blog");
|
||||
logger.debug("Blog cache invalidated");
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate portfolio-related cache
|
||||
*/
|
||||
const invalidatePortfolioCache = () => {
|
||||
cache.deletePattern("portfolio");
|
||||
logger.debug("Portfolio cache invalidated");
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate homepage cache
|
||||
*/
|
||||
const invalidateHomepageCache = () => {
|
||||
cache.deletePattern("homepage");
|
||||
logger.debug("Homepage cache invalidated");
|
||||
};
|
||||
|
||||
/**
|
||||
* Invalidate all caches
|
||||
*/
|
||||
const invalidateAllCache = () => {
|
||||
cache.clear();
|
||||
logger.info("All cache cleared");
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
invalidateProductCache,
|
||||
invalidateBlogCache,
|
||||
invalidatePortfolioCache,
|
||||
invalidateHomepageCache,
|
||||
invalidateAllCache,
|
||||
};
|
||||
52
backend/utils/databaseOptimizations.sql
Normal file
52
backend/utils/databaseOptimizations.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- Database Performance Optimizations for SkyArtShop
|
||||
-- Run these commands to add indexes and optimize queries
|
||||
|
||||
-- Products table indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_products_isactive ON products(isactive) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_products_isfeatured ON products(isfeatured) WHERE isfeatured = true AND isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_products_slug ON products(slug) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_products_createdat ON products(createdat DESC) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_products_composite ON products(isactive, isfeatured, createdat DESC);
|
||||
|
||||
-- Product images indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_product_images_product_id ON product_images(product_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_product_images_is_primary ON product_images(product_id, is_primary) WHERE is_primary = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_product_images_display_order ON product_images(product_id, display_order, created_at);
|
||||
|
||||
-- Blog posts indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_blogposts_ispublished ON blogposts(ispublished) WHERE ispublished = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_blogposts_slug ON blogposts(slug) WHERE ispublished = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_blogposts_createdat ON blogposts(createdat DESC) WHERE ispublished = true;
|
||||
|
||||
-- Portfolio projects indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolio_isactive ON portfolioprojects(isactive) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_portfolio_display ON portfolioprojects(displayorder ASC, createdat DESC) WHERE isactive = true;
|
||||
|
||||
-- Pages indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug) WHERE isactive = true;
|
||||
CREATE INDEX IF NOT EXISTS idx_pages_isactive ON pages(isactive) WHERE isactive = true;
|
||||
|
||||
-- Homepage sections indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_homepagesections_display ON homepagesections(displayorder ASC);
|
||||
|
||||
-- Team members indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_team_members_display ON team_members(display_order ASC, created_at DESC);
|
||||
|
||||
-- Session table optimization (if using pg-session)
|
||||
CREATE INDEX IF NOT EXISTS idx_session_expire ON session(expire);
|
||||
|
||||
-- Analyze tables to update statistics
|
||||
ANALYZE products;
|
||||
ANALYZE product_images;
|
||||
ANALYZE blogposts;
|
||||
ANALYZE portfolioprojects;
|
||||
ANALYZE pages;
|
||||
ANALYZE homepagesections;
|
||||
ANALYZE team_members;
|
||||
|
||||
-- Add comments for documentation
|
||||
COMMENT ON INDEX idx_products_isactive IS 'Optimizes filtering active products';
|
||||
COMMENT ON INDEX idx_products_isfeatured IS 'Optimizes featured products query';
|
||||
COMMENT ON INDEX idx_products_slug IS 'Optimizes product lookup by slug';
|
||||
COMMENT ON INDEX idx_products_composite IS 'Composite index for common query patterns';
|
||||
@@ -1,57 +1,273 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %></title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css">
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center min-vh-100">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-body p-5">
|
||||
<div class="text-center mb-4">
|
||||
<h2 class="fw-bold"><i class="bi bi-shop text-primary"></i> SkyArtShop</h2>
|
||||
<p class="text-muted">Admin Login</p>
|
||||
</div>
|
||||
<% if (error === 'invalid') { %>
|
||||
<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> Invalid email or password</div>
|
||||
<% } else if (error === 'server') { %>
|
||||
<div class="alert alert-danger"><i class="bi bi-exclamation-triangle"></i> Server error. Please try again.</div>
|
||||
<% } %>
|
||||
<form method="POST" action="/admin/login">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
|
||||
<input type="email" class="form-control" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-lock"></i></span>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg"><i class="bi bi-box-arrow-in-right"></i> Login</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
<small class="text-muted"><a href="/" class="text-decoration-none"><i class="bi bi-arrow-left"></i> Back to website</a></small>
|
||||
</div>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= title %></title>
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css"
|
||||
/>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
.logo-section {
|
||||
flex: 1;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
.logo-content {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
.logo-image {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-section {
|
||||
flex: 1;
|
||||
background-color: #ffd0d0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
}
|
||||
.color-code {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.login-box {
|
||||
background: white;
|
||||
border: 2px solid #333;
|
||||
padding: 60px 50px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.login-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
}
|
||||
.login-subtitle {
|
||||
font-size: 1.1rem;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.form-control {
|
||||
border: 2px solid #ddd;
|
||||
padding: 12px 15px;
|
||||
font-size: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #333;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-group-custom {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.btn-custom {
|
||||
flex: 1;
|
||||
padding: 12px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
border: 2px solid #333;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn-login {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
}
|
||||
.btn-login:hover {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
}
|
||||
.btn-reset {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
}
|
||||
.btn-reset:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.back-link a {
|
||||
color: #ff6b6b;
|
||||
text-decoration: none;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.back-link a:hover {
|
||||
color: #ff5252;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.alert {
|
||||
margin-bottom: 25px;
|
||||
border-radius: 4px;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
@media (max-width: 992px) {
|
||||
.login-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
.logo-section,
|
||||
.form-section {
|
||||
flex: none;
|
||||
height: 50vh;
|
||||
}
|
||||
.color-code {
|
||||
font-size: 18px;
|
||||
}
|
||||
.login-box {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.login-box {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<!-- Left Section - Logo -->
|
||||
<div class="logo-section">
|
||||
<div class="logo-content">
|
||||
<img
|
||||
src="/uploads/cat-logo-template-page-20251224-194356-0000-1766724728795-173839741.png"
|
||||
alt="Sky Art Shop Logo"
|
||||
class="logo-image"
|
||||
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';"
|
||||
/>
|
||||
<div style="display: none">
|
||||
<svg
|
||||
width="400"
|
||||
height="300"
|
||||
viewBox="0 0 400 300"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Cat Silhouette -->
|
||||
<path
|
||||
d="M120 80 Q100 60 90 80 L80 100 Q70 120 80 140 L100 160 Q110 180 140 180 L160 185 Q180 190 200 180 L220 160 Q230 140 220 120 L210 100 Q200 80 180 80 Z"
|
||||
fill="#000"
|
||||
/>
|
||||
<circle cx="110" cy="100" r="8" fill="#000" />
|
||||
<circle cx="170" cy="100" r="8" fill="#000" />
|
||||
<!-- Text -->
|
||||
<text
|
||||
x="50"
|
||||
y="240"
|
||||
font-family="'Brush Script MT', cursive"
|
||||
font-size="48"
|
||||
fill="#000"
|
||||
>
|
||||
Sky Art Shop
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">Default: admin@example.com / password</small>
|
||||
</div>
|
||||
|
||||
<!-- Right Section - Form -->
|
||||
<div class="form-section">
|
||||
<div class="color-code">#ffd0d0</div>
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">Sky Art Shop</h1>
|
||||
<p class="login-subtitle">Admin Panel Login</p>
|
||||
|
||||
<% if (error === 'invalid') { %>
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i> Invalid email or password
|
||||
</div>
|
||||
<% } else if (error === 'server') { %>
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle"></i> Server error. Please try
|
||||
again.
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<form method="POST" action="/admin/login">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Username:</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-custom">
|
||||
<button type="submit" class="btn-custom btn-login">Login</button>
|
||||
<button type="reset" class="btn-custom btn-reset">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">Back to Website</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user