Files

324 lines
8.9 KiB
JavaScript

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]
)
return rows[0]
}
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) => {
try {
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 })
} catch (error) {
next(error)
}
})
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.single("image"), 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.file) {
res.status(400).json({ message: "Semua field produk dan foto wajib diisi." })
return
}
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 {
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 3MB." })
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)
}
}