updateweb
This commit is contained in:
65
backend/old-setup-scripts/admin-panel-schema.sql
Normal file
65
backend/old-setup-scripts/admin-panel-schema.sql
Normal file
@@ -0,0 +1,65 @@
|
||||
-- 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';
|
||||
@@ -8,15 +8,15 @@
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-pg-simple": "^9.0.1",
|
||||
"pg": "^8.11.3",
|
||||
"bcrypt": "^5.1.1",
|
||||
"ejs": "^3.1.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"express-validator": "^7.0.1",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"pg": "^8.11.3",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,4 +95,515 @@ router.get("/pages", requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.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) {
|
||||
console.error("Save menu error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -92,7 +92,7 @@ router.get("/homepage/sections", async (req, res) => {
|
||||
router.get("/portfolio/projects", async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, title, description, imageurl, categoryid, createdat FROM portfolioprojects ORDER BY createdat DESC"
|
||||
"SELECT id, title, description, featuredimage, images, category, categoryid, isactive, createdat FROM portfolioprojects WHERE isactive = true ORDER BY displayorder ASC, createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -120,4 +120,101 @@ router.get("/blog/posts", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get single blog post by slug
|
||||
router.get("/blog/posts/:slug", async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT * FROM blogposts WHERE slug = $1 AND ispublished = true",
|
||||
[req.params.slug]
|
||||
);
|
||||
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) {
|
||||
console.error("Blog post detail error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get custom pages
|
||||
router.get("/pages", async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT id, title, slug, content, metatitle, metadescription, isactive, createdat FROM pages WHERE isactive = true ORDER BY createdat DESC"
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
pages: result.rows,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Pages error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single page by slug
|
||||
router.get("/pages/:slug", async (req, res) => {
|
||||
try {
|
||||
const result = await query(
|
||||
"SELECT * FROM pages WHERE slug = $1 AND isactive = true",
|
||||
[req.params.slug]
|
||||
);
|
||||
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) {
|
||||
console.error("Page detail error:", error);
|
||||
res.status(500).json({ success: false, message: "Server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get menu items for frontend navigation
|
||||
router.get("/menu", 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 || [] : [];
|
||||
// Filter only visible items
|
||||
const visibleItems = items.filter((item) => item.visible !== false);
|
||||
res.json({
|
||||
success: true,
|
||||
items: visibleItems,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Menu error:", error);
|
||||
res.json({ success: true, items: [] });
|
||||
}
|
||||
});
|
||||
|
||||
// Get homepage settings for frontend
|
||||
router.get("/homepage/settings", 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) {
|
||||
console.error("Homepage settings error:", error);
|
||||
res.json({ success: true, settings: {} });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
207
backend/routes/upload.js
Normal file
207
backend/routes/upload.js
Normal file
@@ -0,0 +1,207 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const multer = require("multer");
|
||||
const path = require("path");
|
||||
const fs = require("fs").promises;
|
||||
const { requireAuth } = require("../middleware/auth");
|
||||
const { pool } = require("../config/database");
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: async function (req, file, cb) {
|
||||
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
|
||||
try {
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
cb(null, uploadDir);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// Generate unique filename
|
||||
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||
const ext = path.extname(file.originalname);
|
||||
const name = path
|
||||
.basename(file.originalname, ext)
|
||||
.replace(/[^a-z0-9]/gi, "-")
|
||||
.toLowerCase();
|
||||
cb(null, name + "-" + uniqueSuffix + ext);
|
||||
},
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 5 * 1024 * 1024, // 5MB limit
|
||||
},
|
||||
fileFilter: function (req, file, cb) {
|
||||
// Accept images only
|
||||
if (!file.mimetype.startsWith("image/")) {
|
||||
return cb(new Error("Only image files are allowed!"), false);
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
// Upload multiple files
|
||||
router.post(
|
||||
"/upload",
|
||||
requireAuth,
|
||||
upload.array("files", 10),
|
||||
async (req, res) => {
|
||||
try {
|
||||
const uploadedBy = req.session.user?.id || null;
|
||||
const files = [];
|
||||
|
||||
// Insert each file into database
|
||||
for (const file of req.files) {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO uploads
|
||||
(filename, original_name, file_path, file_size, mime_type, uploaded_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
|
||||
RETURNING id, filename, original_name, file_path, file_size, mime_type, created_at`,
|
||||
[
|
||||
file.filename,
|
||||
file.originalname,
|
||||
`/uploads/${file.filename}`,
|
||||
file.size,
|
||||
file.mimetype,
|
||||
uploadedBy,
|
||||
]
|
||||
);
|
||||
|
||||
files.push({
|
||||
id: result.rows[0].id,
|
||||
filename: result.rows[0].filename,
|
||||
originalName: result.rows[0].original_name,
|
||||
size: result.rows[0].file_size,
|
||||
mimetype: result.rows[0].mime_type,
|
||||
path: result.rows[0].file_path,
|
||||
uploadDate: result.rows[0].created_at,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${files.length} file(s) uploaded successfully`,
|
||||
files: files,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
|
||||
// If database insert fails, clean up uploaded files
|
||||
if (req.files) {
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error("Error cleaning up file:", unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Get all uploaded files
|
||||
router.get("/uploads", requireAuth, async (req, res) => {
|
||||
try {
|
||||
// Query files from database
|
||||
const result = await pool.query(
|
||||
`SELECT
|
||||
id,
|
||||
filename,
|
||||
original_name,
|
||||
file_path,
|
||||
file_size,
|
||||
mime_type,
|
||||
uploaded_by,
|
||||
created_at,
|
||||
updated_at,
|
||||
used_in_type,
|
||||
used_in_id
|
||||
FROM uploads
|
||||
ORDER BY created_at DESC`
|
||||
);
|
||||
|
||||
const files = result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
filename: row.filename,
|
||||
originalName: row.original_name,
|
||||
size: row.file_size,
|
||||
mimetype: row.mime_type,
|
||||
path: row.file_path,
|
||||
uploadDate: row.created_at,
|
||||
uploadedBy: row.uploaded_by,
|
||||
usedInType: row.used_in_type,
|
||||
usedInId: row.used_in_id,
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
files: files,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error listing files:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a file
|
||||
router.delete("/uploads/:filename", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const filename = req.params.filename;
|
||||
const uploadDir = path.join(__dirname, "..", "..", "website", "uploads");
|
||||
const filePath = path.join(uploadDir, filename);
|
||||
|
||||
// Security check: ensure file is within uploads directory
|
||||
if (!filePath.startsWith(uploadDir)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: "Invalid file path",
|
||||
});
|
||||
}
|
||||
|
||||
// Start transaction: delete from database first
|
||||
const result = await pool.query(
|
||||
"DELETE FROM uploads WHERE filename = $1 RETURNING id",
|
||||
[filename]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: "File not found in database",
|
||||
});
|
||||
}
|
||||
|
||||
// Then delete physical file
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (fileError) {
|
||||
console.warn("File already deleted from disk:", filename);
|
||||
// Continue anyway since database record is deleted
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "File deleted successfully",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -13,12 +13,9 @@ router.get("/", async (req, res) => {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT
|
||||
u.id, u.username, u.email, u.role_id, u.isactive,
|
||||
u.last_login, u.createdat, u.password_never_expires,
|
||||
u.password_expires_at, u.last_password_change,
|
||||
r.name as role_name, r.description as role_description
|
||||
u.id, u.username, u.email, u.name, u.role, u.isactive,
|
||||
u.last_login, u.createdat, u.passwordneverexpires
|
||||
FROM adminusers u
|
||||
LEFT JOIN roles r ON u.role_id = r.id
|
||||
ORDER BY u.createdat DESC
|
||||
`);
|
||||
|
||||
|
||||
@@ -8,10 +8,17 @@ require("dotenv").config();
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// Serve static files from /var/www/skyartshop
|
||||
app.use(express.static("/var/www/skyartshop/public"));
|
||||
app.use("/assets", express.static("/var/www/skyartshop/assets"));
|
||||
app.use("/uploads", express.static("/var/www/skyartshop/uploads"));
|
||||
// Development mode - Serve static files from development directory
|
||||
const isDevelopment = process.env.NODE_ENV !== "production";
|
||||
const baseDir = isDevelopment
|
||||
? path.join(__dirname, "..", "website")
|
||||
: "/var/www/skyartshop";
|
||||
|
||||
console.log(`📁 Serving from: ${baseDir}`);
|
||||
|
||||
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")));
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -27,13 +34,12 @@ app.use(
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production" ? true : false,
|
||||
secure: false, // Always false for localhost development
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000,
|
||||
sameSite: "lax",
|
||||
domain: process.env.NODE_ENV === "production" ? ".ddns.net" : "localhost",
|
||||
},
|
||||
proxy: true,
|
||||
proxy: false, // No proxy in development
|
||||
name: "skyartshop.sid",
|
||||
})
|
||||
);
|
||||
@@ -49,6 +55,7 @@ const authRoutes = require("./routes/auth");
|
||||
const adminRoutes = require("./routes/admin");
|
||||
const publicRoutes = require("./routes/public");
|
||||
const usersRoutes = require("./routes/users");
|
||||
const uploadRoutes = require("./routes/upload");
|
||||
|
||||
// Admin redirect - handle /admin to redirect to login (must be before static files)
|
||||
app.get("/admin", (req, res) => {
|
||||
@@ -63,14 +70,15 @@ app.get("/admin/", (req, res) => {
|
||||
app.use("/api/admin", authRoutes);
|
||||
app.use("/api/admin", adminRoutes);
|
||||
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("/var/www/skyartshop/admin"));
|
||||
app.use("/admin", express.static(path.join(baseDir, "admin")));
|
||||
|
||||
// Root redirect to admin login
|
||||
// Root redirect to home page
|
||||
app.get("/", (req, res) => {
|
||||
res.redirect("/admin/login.html");
|
||||
res.sendFile(path.join(baseDir, "public", "index.html"));
|
||||
});
|
||||
|
||||
app.get("/health", (req, res) => {
|
||||
|
||||
67
backend/test-navigation.sh
Executable file
67
backend/test-navigation.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/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 ""
|
||||
124
backend/test-upload-db.js
Executable file
124
backend/test-upload-db.js
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/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);
|
||||
34
backend/uploads-schema.sql
Normal file
34
backend/uploads-schema.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- Create uploads table to track all uploaded media files
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL UNIQUE,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
file_path VARCHAR(500) NOT NULL,
|
||||
file_size INTEGER NOT NULL,
|
||||
mime_type VARCHAR(100) NOT NULL,
|
||||
uploaded_by INTEGER REFERENCES adminusers(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create index for faster queries
|
||||
CREATE INDEX IF NOT EXISTS idx_uploads_filename ON uploads(filename);
|
||||
CREATE INDEX IF NOT EXISTS idx_uploads_created_at ON uploads(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_uploads_uploaded_by ON uploads(uploaded_by);
|
||||
|
||||
-- Add column to track which entity uses this upload
|
||||
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS used_in_type VARCHAR(50);
|
||||
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS used_in_id INTEGER;
|
||||
|
||||
-- Create composite index for usage tracking
|
||||
CREATE INDEX IF NOT EXISTS idx_uploads_usage ON uploads(used_in_type, used_in_id);
|
||||
|
||||
COMMENT ON TABLE uploads IS 'Tracks all uploaded media files (images, documents, etc.)';
|
||||
COMMENT ON COLUMN uploads.filename IS 'Unique filename stored on disk';
|
||||
COMMENT ON COLUMN uploads.original_name IS 'Original filename from user upload';
|
||||
COMMENT ON COLUMN uploads.file_path IS 'Relative path to file (e.g., /uploads/image.jpg)';
|
||||
COMMENT ON COLUMN uploads.file_size IS 'File size in bytes';
|
||||
COMMENT ON COLUMN uploads.mime_type IS 'MIME type of the file';
|
||||
COMMENT ON COLUMN uploads.uploaded_by IS 'Admin user who uploaded the file';
|
||||
COMMENT ON COLUMN uploads.used_in_type IS 'Type of entity using this file (product, blog, portfolio, etc.)';
|
||||
COMMENT ON COLUMN uploads.used_in_id IS 'ID of the entity using this file';
|
||||
Reference in New Issue
Block a user