import bcrypt from "bcryptjs" import cors from "cors" import dotenv from "dotenv" import express from "express" import fs from "fs" import jwt from "jsonwebtoken" import multer from "multer" import path from "path" import { fileURLToPath } from "url" import { pool } from "./db.js" dotenv.config() const app = express() const port = Number(process.env.PORT || 5000) const jwtSecret = process.env.JWT_SECRET || "secondtech_dev_secret" const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const uploadsDir = path.join(__dirname, "uploads") if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }) } app.use(cors({ origin: process.env.CLIENT_URL || "http://localhost:5173", credentials: true })) app.use(express.json()) app.use("/uploads", express.static(uploadsDir)) const storage = multer.diskStorage({ destination: uploadsDir, filename: (req, file, cb) => { const ext = path.extname(file.originalname).toLowerCase() cb(null, `${Date.now()}-${Math.round(Math.random() * 1e9)}${ext}`) } }) const upload = multer({ storage, limits: { fileSize: 3 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (!file.mimetype.startsWith("image/")) { cb(new Error("File harus berupa gambar.")) return } cb(null, true) } }) function signUser(user) { return jwt.sign({ id: user.id, email: user.email }, jwtSecret, { expiresIn: "7d" }) } function publicUser(user) { return { id: user.id, name: user.name, email: user.email, whatsapp: user.whatsapp, role: user.role || "user" } } function auth(req, res, next) { const header = req.headers.authorization || "" const token = header.startsWith("Bearer ") ? header.slice(7) : "" if (!token) { res.status(401).json({ message: "Silakan login terlebih dahulu." }) return } try { req.user = jwt.verify(token, jwtSecret) next() } catch { res.status(401).json({ message: "Sesi login tidak valid." }) } } 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] ) const product = rows[0] if (!product) return null const [images] = await pool.query( "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", [id] ) product.images = images.length ? images.map((item) => item.image) : [product.image] return product } app.get("/api/health", async (req, res, next) => { try { await pool.query("SELECT 1") res.json({ status: "ok" }) } catch (error) { next(error) } }) app.post("/api/auth/register", async (req, res, next) => { try { const { name, email, whatsapp, password } = req.body if (!name || !email || !whatsapp || !password) { res.status(400).json({ message: "Semua field wajib diisi." }) return } if (password.length < 6) { res.status(400).json({ message: "Password minimal 6 karakter." }) return } const passwordHash = await bcrypt.hash(password, 10) const [result] = await pool.query( "INSERT INTO users (name, email, whatsapp, password_hash) VALUES (?, ?, ?, ?)", [name, email.toLowerCase(), whatsapp, passwordHash] ) const user = { id: result.insertId, name, email: email.toLowerCase(), whatsapp, role: "user" } res.status(201).json({ token: signUser(user), user: publicUser(user) }) } catch (error) { if (error.code === "ER_DUP_ENTRY") { res.status(409).json({ message: "Email sudah terdaftar." }) return } next(error) } }) app.post("/api/auth/login", async (req, res, next) => { try { const { email, password } = req.body if (!email || !password) { res.status(400).json({ message: "Email dan password wajib diisi." }) return } const [rows] = await pool.query("SELECT * FROM users WHERE email = ?", [email.toLowerCase()]) const user = rows[0] if (!user || !(await bcrypt.compare(password, user.password_hash))) { res.status(401).json({ message: "Email atau password salah." }) return } res.json({ token: signUser(user), user: publicUser(user) }) } catch (error) { next(error) } }) app.get("/api/auth/me", auth, async (req, res, next) => { try { const [rows] = await pool.query("SELECT id, name, email, whatsapp, role FROM users WHERE id = ?", [req.user.id]) if (!rows[0]) { res.status(404).json({ message: "User tidak ditemukan." }) return } res.json({ user: rows[0] }) } catch (error) { next(error) } }) app.get("/api/products", async (req, res, next) => { const products = await Promise.all( rows.map(async (product) => { const [images] = await pool.query( "SELECT image FROM product_images WHERE product_id = ? ORDER BY sort_order ASC, id ASC", [product.id] ) return { ...product, images: images.length ? images.map((item) => item.image) : [product.image] } }) ) res.json({ products }) }) app.get("/api/products/mine", auth, async (req, res, next) => { try { const [rows] = await pool.query( "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 }) } catch (error) { next(error) } }) app.get("/api/products/:id", 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 }) } catch (error) { next(error) } }) app.post("/api/products", auth, upload.array("images", 6), async (req, res, next) => { try { const { title, category, price, condition, location, seller, whatsapp, description } = req.body if (!title || !category || !price || !condition || !location || !seller || !whatsapp || !description || !req.files?.length) { res.status(400).json({ message: "Semua field produk dan minimal 1 foto wajib diisi." }) return } const mainImagePath = `/uploads/${req.files[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 = req.files.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) } }) app.patch("/api/products/:id/status", auth, async (req, res, next) => { try { const status = req.body.status === "Terjual" ? "Terjual" : "Tersedia" const [result] = await pool.query( "UPDATE products SET status = ? WHERE id = ? AND user_id = ?", [status, req.params.id, req.user.id] ) if (!result.affectedRows) { res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) return } const product = await findProductById(req.params.id) res.json({ product }) } catch (error) { next(error) } }) app.delete("/api/products/:id", auth, async (req, res, next) => { try { const product = await findProductById(req.params.id) if (!product || product.user_id !== req.user.id) { res.status(404).json({ message: "Produk milik kamu tidak ditemukan." }) return } 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 }, () => {}) } res.json({ message: "Produk berhasil dihapus." }) } catch (error) { next(error) } }) app.delete("/api/admin/products/:id", auth, superadminOnly, async (req, res, next) => { try { const product = await findProductById(req.params.id) if (!product) { res.status(404).json({ message: "Produk tidak ditemukan." }) return } 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 }, () => {}) } res.json({ message: "Produk berhasil dihapus oleh superadmin." }) } catch (error) { next(error) } }) app.use((error, req, res, next) => { if (error instanceof multer.MulterError) { res.status(400).json({ message: "Upload gagal. Ukuran foto maksimal 10MB." }) return } if (error.message === "File harus berupa gambar.") { res.status(400).json({ message: error.message }) return } console.error(error) res.status(500).json({ message: "Terjadi kesalahan pada server." }) }) 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) } }