fitur fitur darkmode dan foto foto
This commit is contained in:
+163
-55
@@ -14,6 +14,8 @@ dotenv.config()
|
||||
const app = express()
|
||||
const port = Number(process.env.PORT || 5000)
|
||||
const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret"
|
||||
const maxImageSizeMb = Number(process.env.MAX_IMAGE_SIZE_MB || 10)
|
||||
const maxImageCount = Number(process.env.MAX_IMAGE_COUNT || 6)
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const uploadsDir = path.join(__dirname, "uploads")
|
||||
@@ -39,7 +41,7 @@ const storage = multer.diskStorage({
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 3 * 1024 * 1024 },
|
||||
limits: { fileSize: maxImageSizeMb * 1024 * 1024, files: maxImageCount },
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (!file.mimetype.startsWith("image/")) {
|
||||
cb(new Error("File harus berupa gambar."))
|
||||
@@ -49,6 +51,21 @@ const upload = multer({
|
||||
}
|
||||
})
|
||||
|
||||
async function ensureSchema() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS product_images (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
product_id INT UNSIGNED NOT NULL,
|
||||
image VARCHAR(255) NOT NULL,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_product_images_product
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
}
|
||||
|
||||
function signUser(user) {
|
||||
return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" })
|
||||
}
|
||||
@@ -80,12 +97,81 @@ function auth(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
async function superadminOnly(req, res, next) {
|
||||
try {
|
||||
const [rows] = await pool.query("SELECT role FROM users WHERE id = ?", [req.user.id])
|
||||
|
||||
if (!rows[0] || rows[0].role !== "superadmin") {
|
||||
res.status(403).json({ message: "Hanya superadmin yang boleh mengakses fitur ini." })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
|
||||
function uniqueImages(files) {
|
||||
const seen = new Set()
|
||||
return files.filter((file) => {
|
||||
const key = `${file.originalname}-${file.size}-${file.mimetype}`
|
||||
if (seen.has(key)) return false
|
||||
seen.add(key)
|
||||
return true
|
||||
}).slice(0, maxImageCount)
|
||||
}
|
||||
|
||||
function getUploadedImages(req) {
|
||||
const files = [
|
||||
...(req.files?.images || []),
|
||||
...(req.files?.image || []),
|
||||
...(req.files?.photos || [])
|
||||
]
|
||||
return uniqueImages(files)
|
||||
}
|
||||
|
||||
async function getProductImages(productId, fallbackImage) {
|
||||
const [images] = await pool.query(
|
||||
"SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC",
|
||||
[productId]
|
||||
)
|
||||
|
||||
const imagePaths = images.map((item) => item.image).filter(Boolean)
|
||||
if (imagePaths.length) return imagePaths
|
||||
return fallbackImage ? [fallbackImage] : []
|
||||
}
|
||||
|
||||
async function attachProductImages(products) {
|
||||
return Promise.all(
|
||||
products.map(async (product) => ({
|
||||
...product,
|
||||
images: await getProductImages(product.id, product.image)
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
async function findProductById(id) {
|
||||
const [rows] = await pool.query(
|
||||
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE id = ?",
|
||||
[id]
|
||||
)
|
||||
return rows[0]
|
||||
|
||||
const product = rows[0]
|
||||
if (!product) return null
|
||||
|
||||
product.images = await getProductImages(product.id, product.image)
|
||||
return product
|
||||
}
|
||||
|
||||
async function deleteUploadedImages(product) {
|
||||
const images = product.images?.length ? product.images : [product.image]
|
||||
|
||||
images.forEach((image) => {
|
||||
if (image?.startsWith("/uploads/")) {
|
||||
fs.rm(path.join(uploadsDir, path.basename(image)), { force: true }, () => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
app.get("/api/health", async (req, res, next) => {
|
||||
@@ -97,6 +183,24 @@ app.get("/api/health", async (req, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
app.get("/api/debug/products/:id/images", async (req, res, next) => {
|
||||
try {
|
||||
const product = await findProductById(req.params.id)
|
||||
if (!product) {
|
||||
res.status(404).json({ message: "Produk tidak ditemukan." })
|
||||
return
|
||||
}
|
||||
res.json({
|
||||
product_id: Number(req.params.id),
|
||||
main_image: product.image,
|
||||
image_count: product.images.length,
|
||||
images: product.images
|
||||
})
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/api/auth/register", async (req, res, next) => {
|
||||
try {
|
||||
const { name, email, whatsapp, password } = req.body
|
||||
@@ -169,7 +273,7 @@ app.get("/api/products", async (req, res, next) => {
|
||||
const [rows] = await pool.query(
|
||||
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products ORDER BY created_at DESC, id DESC"
|
||||
)
|
||||
res.json({ products: rows })
|
||||
res.json({ products: await attachProductImages(rows) })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
@@ -181,7 +285,7 @@ app.get("/api/products/mine", auth, async (req, res, next) => {
|
||||
"SELECT id, user_id, title, category, price, `condition`, location, seller, whatsapp, image, description, status, created_at FROM products WHERE user_id = ? ORDER BY created_at DESC, id DESC",
|
||||
[req.user.id]
|
||||
)
|
||||
res.json({ products: rows })
|
||||
res.json({ products: await attachProductImages(rows) })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
@@ -200,27 +304,46 @@ app.get("/api/products/:id", async (req, res, next) => {
|
||||
}
|
||||
})
|
||||
|
||||
app.post("/api/products", auth, upload.single("image"), async (req, res, next) => {
|
||||
try {
|
||||
const { title, category, price, condition, location, seller, whatsapp, description } = req.body
|
||||
app.post(
|
||||
"/api/products",
|
||||
auth,
|
||||
upload.fields([
|
||||
{ name: "images", maxCount: maxImageCount },
|
||||
{ name: "image", maxCount: maxImageCount },
|
||||
{ name: "photos", maxCount: maxImageCount }
|
||||
]),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const { title, category, price, condition, location, seller, whatsapp, description } = req.body
|
||||
const uploadedImages = getUploadedImages(req)
|
||||
|
||||
if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.file) {
|
||||
res.status(400).json({ message: "Semua field produk dan foto wajib diisi." })
|
||||
return
|
||||
console.log("Upload produk:", {
|
||||
title,
|
||||
imageCount: uploadedImages.length,
|
||||
files: uploadedImages.map((file) => file.originalname)
|
||||
})
|
||||
|
||||
if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !uploadedImages.length) {
|
||||
res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." })
|
||||
return
|
||||
}
|
||||
|
||||
const mainImagePath = `/uploads/${uploadedImages[0].filename}`
|
||||
const [result] = await pool.query(
|
||||
"INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[req.user.id, title, category, Number(price), condition, location, seller, whatsapp, mainImagePath, description]
|
||||
)
|
||||
|
||||
const imageRows = uploadedImages.map((file, index) => [result.insertId, `/uploads/${file.filename}`, index])
|
||||
await pool.query("INSERT INTO product_images (product_id, image, sort_order) VALUES ?", [imageRows])
|
||||
|
||||
const product = await findProductById(result.insertId)
|
||||
res.status(201).json({ product })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
|
||||
const imagePath = `/uploads/${req.file.filename}`
|
||||
const [result] = await pool.query(
|
||||
"INSERT INTO products (user_id, title, category, price, `condition`, location, seller, whatsapp, image, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[req.user.id, title, category, Number(price), condition, location, seller, whatsapp, imagePath, description]
|
||||
)
|
||||
|
||||
const product = await findProductById(result.insertId)
|
||||
res.status(201).json({ product })
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
app.patch("/api/products/:id/status", auth, async (req, res, next) => {
|
||||
try {
|
||||
@@ -251,10 +374,7 @@ app.delete("/api/products/:id", auth, async (req, res, next) => {
|
||||
}
|
||||
|
||||
await pool.query("DELETE FROM products WHERE id = ? AND user_id = ?", [req.params.id, req.user.id])
|
||||
|
||||
if (product.image.startsWith("/uploads/")) {
|
||||
fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {})
|
||||
}
|
||||
await deleteUploadedImages(product)
|
||||
|
||||
res.json({ message: "Produk berhasil dihapus." })
|
||||
} catch (error) {
|
||||
@@ -272,10 +392,7 @@ app.delete("/api/admin/products/:id", auth, superadminOnly, async (req, res, nex
|
||||
}
|
||||
|
||||
await pool.query("DELETE FROM products WHERE id = ?", [req.params.id])
|
||||
|
||||
if (product.image.startsWith("/uploads/")) {
|
||||
fs.rm(path.join(uploadsDir, path.basename(product.image)), { force: true }, () => {})
|
||||
}
|
||||
await deleteUploadedImages(product)
|
||||
|
||||
res.json({ message: "Produk berhasil dihapus oleh superadmin." })
|
||||
} catch (error) {
|
||||
@@ -283,10 +400,9 @@ app.delete("/api/admin/products/:id", auth, superadminOnly, async (req, res, nex
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
app.use((error, req, res, next) => {
|
||||
if (error instanceof multer.MulterError) {
|
||||
res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 3MB." })
|
||||
res.status(400).json({ message: `Upload gagal. Ukuran setiap foto maksimal ${maxImageSizeMb} MB dan maksimal ${maxImageCount} foto.` })
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,28 +412,20 @@ app.use((error, req, res, next) => {
|
||||
}
|
||||
|
||||
console.error(error)
|
||||
res.status(500).json({ message: "Terjadi kesalahan pada server." })
|
||||
res.status(500).json({
|
||||
message: "Terjadi kesalahan pada server.",
|
||||
detail: process.env.NODE_ENV === "production" ? undefined : error.message
|
||||
})
|
||||
})
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`SecondTech API berjalan di http://localhost:${port}`)
|
||||
})
|
||||
|
||||
|
||||
async function superadminOnly(req, res, next) {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
"SELECT role FROM users WHERE id = ?",
|
||||
[req.user.id]
|
||||
)
|
||||
|
||||
if (!rows[0] || rows[0].role !== "superadmin") {
|
||||
res.status(403).json({ message: "Hanya superadmin yang boleh mengakses fitur ini." })
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
next(error)
|
||||
}
|
||||
}
|
||||
ensureSchema()
|
||||
.then(() => {
|
||||
app.listen(port, () => {
|
||||
console.log(`SecondTech API berjalan di http://localhost:${port}`)
|
||||
console.log(`Upload foto: maksimal ${maxImageSizeMb} MB per foto, maksimal ${maxImageCount} foto.`)
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Gagal menyiapkan database:", error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 826 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 990 KiB |
Reference in New Issue
Block a user