This commit is contained in:
Local Server
2025-12-19 20:44:46 -06:00
parent 701f799cde
commit e4b3de4a46
113 changed files with 16673 additions and 2174 deletions

264
config/database-fixes.sql Normal file
View File

@@ -0,0 +1,264 @@
-- SkyArtShop Database Schema Fixes
-- Date: December 18, 2025
-- Purpose: Fix missing columns, add constraints, optimize queries
-- =====================================================
-- PHASE 1: Fix Missing Columns
-- =====================================================
-- Fix pages table - add ispublished column
ALTER TABLE pages
ADD COLUMN IF NOT EXISTS ispublished BOOLEAN DEFAULT true;
-- Standardize pages table columns
UPDATE pages SET ispublished = isactive WHERE ispublished IS NULL;
-- Fix portfolioprojects table - add imageurl column
ALTER TABLE portfolioprojects
ADD COLUMN IF NOT EXISTS imageurl VARCHAR(500);
-- Migrate featuredimage to imageurl for consistency
UPDATE portfolioprojects
SET imageurl = featuredimage
WHERE imageurl IS NULL AND featuredimage IS NOT NULL;
-- =====================================================
-- PHASE 2: Add Missing Indexes for Performance
-- =====================================================
-- Products table indexes
CREATE INDEX IF NOT EXISTS idx_products_isactive ON products(isactive);
CREATE INDEX IF NOT EXISTS idx_products_category ON products(category);
CREATE INDEX IF NOT EXISTS idx_products_isfeatured ON products(isfeatured) WHERE isfeatured = true;
CREATE INDEX IF NOT EXISTS idx_products_createdat ON products(createdat DESC);
CREATE INDEX IF NOT EXISTS idx_products_slug ON products(slug);
-- Blog posts indexes
CREATE INDEX IF NOT EXISTS idx_blogposts_ispublished ON blogposts(ispublished);
CREATE INDEX IF NOT EXISTS idx_blogposts_slug ON blogposts(slug);
CREATE INDEX IF NOT EXISTS idx_blogposts_createdat ON blogposts(createdat DESC);
-- Portfolio projects indexes
CREATE INDEX IF NOT EXISTS idx_portfolioprojects_isactive ON portfolioprojects(isactive);
CREATE INDEX IF NOT EXISTS idx_portfolioprojects_categoryid ON portfolioprojects(categoryid);
CREATE INDEX IF NOT EXISTS idx_portfolioprojects_displayorder ON portfolioprojects(displayorder);
-- Pages indexes
CREATE INDEX IF NOT EXISTS idx_pages_isactive ON pages(isactive);
CREATE INDEX IF NOT EXISTS idx_pages_slug ON pages(slug);
-- Admin users indexes
CREATE INDEX IF NOT EXISTS idx_adminusers_email ON adminusers(email);
CREATE INDEX IF NOT EXISTS idx_adminusers_isactive ON adminusers(isactive);
CREATE INDEX IF NOT EXISTS idx_adminusers_role_id ON adminusers(role_id);
-- Session index (for cleanup)
CREATE INDEX IF NOT EXISTS idx_session_expire ON session(expire);
-- =====================================================
-- PHASE 3: Add Constraints and Validations
-- =====================================================
-- Ensure NOT NULL constraints where appropriate
ALTER TABLE products
ALTER COLUMN isactive SET DEFAULT true,
ALTER COLUMN isfeatured SET DEFAULT false,
ALTER COLUMN isbestseller SET DEFAULT false,
ALTER COLUMN stockquantity SET DEFAULT 0;
ALTER TABLE blogposts
ALTER COLUMN ispublished SET DEFAULT false,
ALTER COLUMN views SET DEFAULT 0;
ALTER TABLE portfolioprojects
ALTER COLUMN isactive SET DEFAULT true,
ALTER COLUMN displayorder SET DEFAULT 0;
ALTER TABLE pages
ALTER COLUMN isactive SET DEFAULT true;
-- Add unique constraints
ALTER TABLE products DROP CONSTRAINT IF EXISTS unique_products_slug;
ALTER TABLE products ADD CONSTRAINT unique_products_slug UNIQUE(slug);
ALTER TABLE blogposts DROP CONSTRAINT IF EXISTS unique_blogposts_slug;
ALTER TABLE blogposts ADD CONSTRAINT unique_blogposts_slug UNIQUE(slug);
ALTER TABLE pages DROP CONSTRAINT IF EXISTS unique_pages_slug;
ALTER TABLE pages ADD CONSTRAINT unique_pages_slug UNIQUE(slug);
ALTER TABLE adminusers DROP CONSTRAINT IF EXISTS unique_adminusers_email;
ALTER TABLE adminusers ADD CONSTRAINT unique_adminusers_email UNIQUE(email);
-- Add check constraints
ALTER TABLE products DROP CONSTRAINT IF EXISTS check_products_price_positive;
ALTER TABLE products ADD CONSTRAINT check_products_price_positive CHECK (price >= 0);
ALTER TABLE products DROP CONSTRAINT IF EXISTS check_products_stock_nonnegative;
ALTER TABLE products ADD CONSTRAINT check_products_stock_nonnegative CHECK (stockquantity >= 0);
-- =====================================================
-- PHASE 4: Add Foreign Key Constraints
-- =====================================================
-- Portfolio projects to categories (if portfoliocategories table exists)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'portfoliocategories') THEN
ALTER TABLE portfolioprojects DROP CONSTRAINT IF EXISTS fk_portfolioprojects_category;
ALTER TABLE portfolioprojects
ADD CONSTRAINT fk_portfolioprojects_category
FOREIGN KEY (categoryid) REFERENCES portfoliocategories(id)
ON DELETE SET NULL;
END IF;
END $$;
-- =====================================================
-- PHASE 5: Optimize Existing Data
-- =====================================================
-- Ensure all timestamps are set
UPDATE products SET updatedat = createdat WHERE updatedat IS NULL;
UPDATE blogposts SET updatedat = createdat WHERE updatedat IS NULL;
UPDATE portfolioprojects SET updatedat = createdat WHERE updatedat IS NULL;
UPDATE pages SET updatedat = createdat WHERE updatedat IS NULL;
-- Set default values for nullable booleans
UPDATE products SET isactive = true WHERE isactive IS NULL;
UPDATE products SET isfeatured = false WHERE isfeatured IS NULL;
UPDATE products SET isbestseller = false WHERE isbestseller IS NULL;
UPDATE blogposts SET ispublished = false WHERE ispublished IS NULL;
UPDATE portfolioprojects SET isactive = true WHERE isactive IS NULL;
UPDATE pages SET isactive = true WHERE isactive IS NULL;
UPDATE pages SET ispublished = true WHERE ispublished IS NULL;
-- =====================================================
-- PHASE 6: Create Useful Views for Reporting
-- =====================================================
-- View for active products with sales data
CREATE OR REPLACE VIEW v_active_products AS
SELECT
id, name, slug, price, stockquantity, category,
imageurl, isfeatured, isbestseller,
unitssold, totalrevenue, averagerating, totalreviews,
createdat
FROM products
WHERE isactive = true
ORDER BY createdat DESC;
-- View for published blog posts
CREATE OR REPLACE VIEW v_published_blogposts AS
SELECT
id, title, slug, excerpt, imageurl,
authorname, publisheddate, views, tags,
createdat
FROM blogposts
WHERE ispublished = true
ORDER BY publisheddate DESC NULLS LAST, createdat DESC;
-- View for active portfolio projects
CREATE OR REPLACE VIEW v_active_portfolio AS
SELECT
id, title, description, imageurl, featuredimage,
category, categoryid, displayorder,
createdat
FROM portfolioprojects
WHERE isactive = true
ORDER BY displayorder ASC, createdat DESC;
-- =====================================================
-- PHASE 7: Add Triggers for Automatic Timestamps
-- =====================================================
-- Function to update timestamp
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updatedat = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Add triggers for products
DROP TRIGGER IF EXISTS trg_products_update ON products;
CREATE TRIGGER trg_products_update
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
-- Add triggers for blogposts
DROP TRIGGER IF EXISTS trg_blogposts_update ON blogposts;
CREATE TRIGGER trg_blogposts_update
BEFORE UPDATE ON blogposts
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
-- Add triggers for portfolioprojects
DROP TRIGGER IF EXISTS trg_portfolioprojects_update ON portfolioprojects;
CREATE TRIGGER trg_portfolioprojects_update
BEFORE UPDATE ON portfolioprojects
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
-- Add triggers for pages
DROP TRIGGER IF EXISTS trg_pages_update ON pages;
CREATE TRIGGER trg_pages_update
BEFORE UPDATE ON pages
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
-- Add triggers for adminusers
DROP TRIGGER IF EXISTS trg_adminusers_update ON adminusers;
CREATE TRIGGER trg_adminusers_update
BEFORE UPDATE ON adminusers
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
-- =====================================================
-- PHASE 8: Clean Up Expired Sessions
-- =====================================================
-- Delete expired sessions (run periodically)
DELETE FROM session WHERE expire < NOW();
-- =====================================================
-- VERIFICATION QUERIES
-- =====================================================
-- Verify all critical columns exist
SELECT
'products' as table_name,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='products' AND column_name='isactive') as has_isactive,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='products' AND column_name='isfeatured') as has_isfeatured;
SELECT
'pages' as table_name,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='pages' AND column_name='ispublished') as has_ispublished,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='pages' AND column_name='isactive') as has_isactive;
SELECT
'portfolioprojects' as table_name,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='portfolioprojects' AND column_name='imageurl') as has_imageurl,
EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name='portfolioprojects' AND column_name='isactive') as has_isactive;
-- Count indexes
SELECT
schemaname,
tablename,
COUNT(*) as index_count
FROM pg_indexes
WHERE schemaname = 'public'
GROUP BY schemaname, tablename
ORDER BY tablename;
-- Verify constraints
SELECT
tc.table_name,
tc.constraint_type,
COUNT(*) as constraint_count
FROM information_schema.table_constraints tc
WHERE tc.table_schema = 'public'
GROUP BY tc.table_name, tc.constraint_type
ORDER BY tc.table_name;

View File

@@ -0,0 +1,22 @@
module.exports = {
apps: [
{
name: "skyartshop",
script: "server.js",
cwd: "/media/pts/Website/SkyArtShop/backend",
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "500M",
// Environment variables are loaded from .env file via dotenv
// Do not hardcode sensitive information here
env_file: "/media/pts/Website/SkyArtShop/.env",
error_file: "/var/log/skyartshop/pm2-error.log",
out_file: "/var/log/skyartshop/pm2-output.log",
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
merge_logs: true,
kill_timeout: 5000,
listen_timeout: 10000,
},
],
};

View File

@@ -0,0 +1,171 @@
# HTTP Server - Serves both localhost and redirects skyarts.ddns.net to HTTPS
server {
listen 80;
listen [::]:80;
server_name localhost skyarts.ddns.net;
# Redirect HTTPS for skyarts.ddns.net only
if ($host = skyarts.ddns.net) {
return 301 https://$server_name$request_uri;
}
# For localhost, serve the site
# Logs
access_log /var/log/nginx/skyartshop-access.log;
error_log /var/log/nginx/skyartshop-error.log;
# Root directory
root /var/www/skyartshop/public;
index index.html;
# Admin area - exact matches to redirect
location = /admin {
return 302 /admin/login.html;
}
location = /admin/ {
return 302 /admin/login.html;
}
# API proxy to Node.js backend
location /api/ {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files
location /assets/ {
alias /var/www/skyartshop/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /var/www/skyartshop/uploads/;
expires 30d;
add_header Cache-Control "public";
}
# Admin static files
location /admin/ {
alias /var/www/skyartshop/admin/;
try_files $uri $uri/ =404;
}
# Root redirect handled by backend
location = / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check
location /health {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}
# HTTPS - Main Server for skyarts.ddns.net
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name skyarts.ddns.net;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/skyarts.ddns.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/skyarts.ddns.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Security Headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Logs
access_log /var/log/nginx/skyartshop-access.log;
error_log /var/log/nginx/skyartshop-error.log;
# Root directory
root /var/www/skyartshop/public;
index index.html;
# Admin area - exact matches to redirect
location = /admin {
return 302 /admin/login.html;
}
location = /admin/ {
return 302 /admin/login.html;
}
# API proxy to Node.js backend
location /api/ {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Static files
location /assets/ {
alias /var/www/skyartshop/assets/;
expires 30d;
add_header Cache-Control "public, immutable";
}
location /uploads/ {
alias /var/www/skyartshop/uploads/;
expires 30d;
add_header Cache-Control "public";
}
# Admin static files
location /admin/ {
alias /var/www/skyartshop/admin/;
try_files $uri $uri/ =404;
}
# Root redirect handled by backend
location = / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check
location /health {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
}

View File

@@ -0,0 +1,199 @@
# Upstream configuration
upstream skyartshop_backend {
server localhost:5000;
keepalive 64;
}
# Rate limiting zones - Enhanced for security
limit_req_zone $binary_remote_addr zone=general:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=admin:10m rate=10r/s; # Reduced from 20 to 10
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; # Only 5 login attempts per minute
limit_conn_zone $binary_remote_addr zone=addr:10m;
# Block common attack patterns
map $request_uri $is_blocked {
default 0;
"~*\.(asp|aspx|php|jsp|cgi)$" 1;
# Block common SQL injection keywords except allow legitimate admin endpoints
"~*(eval|base64|decode|union|select|insert|update|drop)" 1;
"~*(/\.|\.\./)" 1;
}
server {
server_name skyarts.ddns.net localhost;
# Block malicious requests
if ($is_blocked = 1) {
return 444;
}
# Enhanced Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# Content Security Policy - Allow only trusted CDNs
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://cdn.ckeditor.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://cdn.jsdelivr.net https://cdn.ckeditor.com; frame-src 'none';" always;
# Prevent clickjacking
add_header X-Permitted-Cross-Domain-Policies "none" always;
# HSTS - Force HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Connection limits
limit_conn addr 10;
# General rate limiting
limit_req zone=general burst=200 nodelay;
# Logging
access_log /var/log/nginx/skyartshop_access.log combined buffer=32k;
error_log /var/log/nginx/skyartshop_error.log warn;
# Block access to sensitive files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block access to config files
location ~* \.(conf|config|json|lock|git|env)$ {
deny all;
}
# Admin root redirect - exact matches for /admin and /admin/
location = /admin {
limit_req zone=admin burst=10 nodelay;
proxy_pass http://skyartshop_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 1M;
}
location = /admin/ {
limit_req zone=admin burst=10 nodelay;
proxy_pass http://skyartshop_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 1M;
}
# API routes - proxy to backend
location /api/ {
limit_req zone=general burst=100 nodelay;
proxy_pass http://skyartshop_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
client_max_body_size 50M;
}
# Admin static files (HTML, CSS, JS)
location /admin/ {
limit_req zone=admin burst=20 nodelay;
alias /var/www/skyartshop/admin/;
try_files $uri $uri/ =404;
# Cache static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 7d;
add_header Cache-Control "public, immutable";
}
# No cache for HTML files
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
}
# Main application - catch all
location / {
proxy_pass http://skyartshop_backend;
proxy_http_version 1.1;
# Connection headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Forwarding headers
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffer settings for performance
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 24 4k;
proxy_busy_buffers_size 8k;
proxy_max_temp_file_size 2048m;
client_max_body_size 50M;
}
listen [::]:443 ssl ipv6only=on; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/skyarts.ddns.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/skyarts.ddns.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = skyarts.ddns.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name skyarts.ddns.net localhost;
return 404; # managed by Certbot
}

30
config/skyartshop.service Normal file
View File

@@ -0,0 +1,30 @@
[Unit]
Description=SkyArtShop Node.js Backend Server
Documentation=https://github.com/yourusername/skyartshop
After=network.target postgresql.service
[Service]
Type=simple
User=pts
WorkingDirectory=/media/pts/Website/SkyArtShop/backend
EnvironmentFile=/media/pts/Website/SkyArtShop/backend/.env
ExecStartPre=/media/pts/Website/SkyArtShop/pre-start.sh
ExecStart=/usr/bin/node /media/pts/Website/SkyArtShop/backend/server.js
Restart=always
RestartSec=10
StartLimitInterval=200
StartLimitBurst=5
StandardOutput=append:/var/log/skyartshop/output.log
StandardError=append:/var/log/skyartshop/error.log
# Restart service after 10 seconds if node service crashes
RestartSec=10
# Output to systemd journal for easy debugging
SyslogIdentifier=skyartshop
# Security settings
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target