updateweb

This commit is contained in:
Local Server
2026-01-01 22:24:30 -06:00
parent 017c6376fc
commit 1919f6f8bb
185 changed files with 19860 additions and 17603 deletions

View File

@@ -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
View 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
View 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

View 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();

View File

@@ -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
View 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
View 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,
};

View 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;

View File

@@ -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",

View File

@@ -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';

View File

@@ -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 "=========================================="

View File

@@ -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 "========================================="

View File

@@ -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 ""

View File

@@ -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 "================================================"

View File

@@ -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"

View File

@@ -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();

View File

@@ -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"

View File

@@ -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 "=========================================="

View File

@@ -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);
});

View File

@@ -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';`
);
});

View File

@@ -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 "=========================================="

View File

@@ -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 '========================================='

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;

View File

@@ -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();

View File

@@ -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",

View File

@@ -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"
}
}

View 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

View File

@@ -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
View 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
```

View File

@@ -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;">

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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");

View 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
View 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;

View 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;

View File

@@ -0,0 +1,3 @@
# Controllers go here
# Each controller handles HTTP requests for a specific resource
# Example: productController.ts, authController.ts, userController.ts

View 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;
}
}

View 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();
}

View 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();
}

View 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,
});
}

View 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();
}

View File

@@ -0,0 +1,3 @@
# Models/Repositories go here
# Database access layer and query methods
# Example: Product.ts, User.ts, Order.ts

View 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
View 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;

View File

@@ -0,0 +1,3 @@
# Services go here
# Contains business logic, data processing, and orchestration
# Example: productService.ts, authService.ts, emailService.ts

View 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);
}
}

View File

@@ -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();

View File

@@ -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 ""

View File

@@ -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>

View File

@@ -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();

View File

@@ -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);
});

View File

@@ -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 ""

View File

@@ -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
View 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"
]
}

View 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();

View 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,
};

View 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';

View File

@@ -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>